From ba4a543ab417498535a9ff5f31b46d2b128b8079 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 19 Jul 2022 15:50:45 +0100 Subject: [PATCH 01/20] Raise exemplar labels limit from 64 to 128 (#1091) In line with the OpenMetrics spec: "The combined length of the label names and values of an Exemplar's LabelSet MUST NOT exceed 128 UTF-8 character code points" https://github.com/OpenObservability/OpenMetrics/blob/98ae26c87b/specification/OpenMetrics.md#exemplars Signed-off-by: Bryan Boreham --- prometheus/counter_test.go | 4 +++- prometheus/value.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/prometheus/counter_test.go b/prometheus/counter_test.go index 40ba427..9398e28 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -16,6 +16,7 @@ package prometheus import ( "fmt" "math" + "strings" "testing" "time" @@ -262,10 +263,11 @@ func TestCounterExemplar(t *testing.T) { err = e.(error) } }() - // Should panic because of 65 runes. + // Should panic because of 129 runes. counter.AddWithExemplar(42, Labels{ "abcdefghijklmnopqrstuvwxyz": "26+16 characters", "x1234567": "8+15 characters", + "z": strings.Repeat("x", 63), }) return nil } diff --git a/prometheus/value.go b/prometheus/value.go index 2dfa659..2d3abc1 100644 --- a/prometheus/value.go +++ b/prometheus/value.go @@ -200,7 +200,7 @@ func MakeLabelPairs(desc *Desc, labelValues []string) []*dto.LabelPair { } // ExemplarMaxRunes is the max total number of runes allowed in exemplar labels. -const ExemplarMaxRunes = 64 +const ExemplarMaxRunes = 128 // newExemplar creates a new dto.Exemplar from the provided values. An error is // returned if any of the label names or values are invalid or if the total From a528affed905f64ed33ed2f52b7880ed7247f99e Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Wed, 27 Jul 2022 17:45:49 +0200 Subject: [PATCH 02/20] Update documentation for exemplar label limit (#1095) Signed-off-by: Fredrik Enestad --- prometheus/counter.go | 2 +- prometheus/observer.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prometheus/counter.go b/prometheus/counter.go index 00d70f0..de30de6 100644 --- a/prometheus/counter.go +++ b/prometheus/counter.go @@ -51,7 +51,7 @@ type Counter interface { // will lead to a valid (label-less) exemplar. But if Labels is nil, the current // exemplar is left in place. AddWithExemplar panics if the value is < 0, if any // of the provided labels are invalid, or if the provided labels contain more -// than 64 runes in total. +// than 128 runes in total. type ExemplarAdder interface { AddWithExemplar(value float64, exemplar Labels) } diff --git a/prometheus/observer.go b/prometheus/observer.go index 4412801..03773b2 100644 --- a/prometheus/observer.go +++ b/prometheus/observer.go @@ -58,7 +58,7 @@ type ObserverVec interface { // current time as timestamp, and the provided Labels. Empty Labels will lead to // a valid (label-less) exemplar. But if Labels is nil, the current exemplar is // left in place. ObserveWithExemplar panics if any of the provided labels are -// invalid or if the provided labels contain more than 64 runes in total. +// invalid or if the provided labels contain more than 128 runes in total. type ExemplarObserver interface { ObserveWithExemplar(value float64, exemplar Labels) } From 9154d30db702ce0b68a531f3724852c089d8fdab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Aug 2022 10:23:53 +0200 Subject: [PATCH 03/20] Bump github.com/prometheus/common from 0.35.0 to 0.37.0 (#1098) Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.35.0 to 0.37.0. - [Release notes](https://github.com/prometheus/common/releases) - [Commits](https://github.com/prometheus/common/compare/v0.35.0...v0.37.0) --- updated-dependencies: - dependency-name: github.com/prometheus/common dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a0ca87b..19ac5d5 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/golang/protobuf v1.5.2 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_model v0.2.0 - github.com/prometheus/common v0.35.0 + github.com/prometheus/common v0.37.0 github.com/prometheus/procfs v0.7.3 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a google.golang.org/protobuf v1.28.0 diff --git a/go.sum b/go.sum index 2b9d6e1..15db9e2 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.35.0 h1:Eyr+Pw2VymWejHqCugNaQXkAi6KayVNxaHeu6khmFBE= -github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= From 76cdae298edf666041809272cdd829717927cd19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Aug 2022 10:24:34 +0200 Subject: [PATCH 04/20] Bump google.golang.org/protobuf from 1.28.0 to 1.28.1 (#1099) Bumps [google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go) from 1.28.0 to 1.28.1. - [Release notes](https://github.com/protocolbuffers/protobuf-go/releases) - [Changelog](https://github.com/protocolbuffers/protobuf-go/blob/master/release.bash) - [Commits](https://github.com/protocolbuffers/protobuf-go/compare/v1.28.0...v1.28.1) --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 19ac5d5..c0c94d9 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/prometheus/common v0.37.0 github.com/prometheus/procfs v0.7.3 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a - google.golang.org/protobuf v1.28.0 + google.golang.org/protobuf v1.28.1 ) require ( diff --git a/go.sum b/go.sum index 15db9e2..9bc26ed 100644 --- a/go.sum +++ b/go.sum @@ -395,8 +395,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= From 44c2c4de85cf72340cbc95f0146146be8c2578ad Mon Sep 17 00:00:00 2001 From: inosato <48383032+inosato@users.noreply.github.com> Date: Tue, 2 Aug 2022 17:27:49 +0900 Subject: [PATCH 05/20] Remove ioutil (#1096) Signed-off-by: inosato --- api/prometheus/v1/api_test.go | 4 ++-- prometheus/process_collector.go | 3 +-- prometheus/push/push.go | 6 +++--- prometheus/push/push_test.go | 4 ++-- prometheus/registry.go | 3 +-- prometheus/registry_test.go | 5 ++--- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/api/prometheus/v1/api_test.go b/api/prometheus/v1/api_test.go index 9fbd29b..009035d 100644 --- a/api/prometheus/v1/api_test.go +++ b/api/prometheus/v1/api_test.go @@ -17,7 +17,7 @@ import ( "context" "errors" "fmt" - "io/ioutil" + "io" "math" "net/http" "net/http/httptest" @@ -1680,7 +1680,7 @@ func (c *httpTestClient) Do(ctx context.Context, req *http.Request) (*http.Respo var body []byte done := make(chan struct{}) go func() { - body, err = ioutil.ReadAll(resp.Body) + body, err = io.ReadAll(resp.Body) close(done) }() diff --git a/prometheus/process_collector.go b/prometheus/process_collector.go index 5bfe0ff..1245428 100644 --- a/prometheus/process_collector.go +++ b/prometheus/process_collector.go @@ -16,7 +16,6 @@ package prometheus import ( "errors" "fmt" - "io/ioutil" "os" "strconv" "strings" @@ -152,7 +151,7 @@ func (c *processCollector) reportError(ch chan<- Metric, desc *Desc, err error) // It is meant to be used for the PidFn field in ProcessCollectorOpts. func NewPidFileFn(pidFilePath string) func() (int, error) { return func() (int, error) { - content, err := ioutil.ReadFile(pidFilePath) + content, err := os.ReadFile(pidFilePath) if err != nil { return 0, fmt.Errorf("can't read pid file %q: %+v", pidFilePath, err) } diff --git a/prometheus/push/push.go b/prometheus/push/push.go index 9d03d8b..06dee37 100644 --- a/prometheus/push/push.go +++ b/prometheus/push/push.go @@ -40,7 +40,7 @@ import ( "encoding/base64" "errors" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -245,7 +245,7 @@ func (p *Pusher) Delete() error { } defer resp.Body.Close() if resp.StatusCode != http.StatusAccepted { - body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. + body, _ := io.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. return fmt.Errorf("unexpected status code %d while deleting %s: %s", resp.StatusCode, p.fullURL(), body) } return nil @@ -297,7 +297,7 @@ func (p *Pusher) push(ctx context.Context, method string) error { defer resp.Body.Close() // 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. + body, _ := io.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 nil diff --git a/prometheus/push/push_test.go b/prometheus/push/push_test.go index 3367a4b..a0e8e9a 100644 --- a/prometheus/push/push_test.go +++ b/prometheus/push/push_test.go @@ -15,7 +15,7 @@ package push import ( "bytes" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" @@ -38,7 +38,7 @@ func TestPush(t *testing.T) { http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lastMethod = r.Method var err error - lastBody, err = ioutil.ReadAll(r.Body) + lastBody, err = io.ReadAll(r.Body) if err != nil { t.Fatal(err) } diff --git a/prometheus/registry.go b/prometheus/registry.go index eda2892..36fd64e 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -16,7 +16,6 @@ package prometheus import ( "bytes" "fmt" - "io/ioutil" "os" "path/filepath" "runtime" @@ -563,7 +562,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { // This is intended for use with the textfile collector of the node exporter. // Note that the node exporter expects the filename to be suffixed with ".prom". func WriteToTextfile(filename string, g Gatherer) error { - tmp, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)) + tmp, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)) if err != nil { return err } diff --git a/prometheus/registry_test.go b/prometheus/registry_test.go index 0a3d746..9443ef3 100644 --- a/prometheus/registry_test.go +++ b/prometheus/registry_test.go @@ -23,7 +23,6 @@ import ( "bytes" "errors" "fmt" - "io/ioutil" "math/rand" "net/http" "net/http/httptest" @@ -1066,7 +1065,7 @@ test_summary_count{name="foo"} 2 gauge.With(prometheus.Labels{"name": "baz"}).Set(1.1) counter.With(prometheus.Labels{"name": "qux"}).Inc() - tmpfile, err := ioutil.TempFile("", "prom_registry_test") + tmpfile, err := os.CreateTemp("", "prom_registry_test") if err != nil { t.Fatal(err) } @@ -1076,7 +1075,7 @@ test_summary_count{name="foo"} 2 t.Fatal(err) } - fileBytes, err := ioutil.ReadFile(tmpfile.Name()) + fileBytes, err := os.ReadFile(tmpfile.Name()) if err != nil { t.Fatal(err) } From c6d4e402442a193d67f715c1d862e12e45e87757 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Aug 2022 10:43:54 +0200 Subject: [PATCH 06/20] Bump github.com/prometheus/procfs from 0.7.3 to 0.8.0 (#1097) Bumps [github.com/prometheus/procfs](https://github.com/prometheus/procfs) from 0.7.3 to 0.8.0. - [Release notes](https://github.com/prometheus/procfs/releases) - [Commits](https://github.com/prometheus/procfs/compare/v0.7.3...v0.8.0) --- updated-dependencies: - dependency-name: github.com/prometheus/procfs dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index c0c94d9..c7da06e 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/prometheus/client_model v0.2.0 github.com/prometheus/common v0.37.0 - github.com/prometheus/procfs v0.7.3 + github.com/prometheus/procfs v0.8.0 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a google.golang.org/protobuf v1.28.1 ) diff --git a/go.sum b/go.sum index 9bc26ed..ad38f27 100644 --- a/go.sum +++ b/go.sum @@ -91,9 +91,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -138,8 +138,8 @@ 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.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -232,7 +232,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -258,7 +258,6 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= @@ -317,7 +316,6 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= From 807b1ee73c2079346d041019cffb933fa43aaf3d Mon Sep 17 00:00:00 2001 From: Arun Mahendra <89400134+arun-shopify@users.noreply.github.com> Date: Tue, 2 Aug 2022 04:48:18 -0400 Subject: [PATCH 07/20] explicitly add +inf bucket in withExemplarsMetric (#1094) * explicitly adding +inf bucket to withExemplarsMetric Signed-off-by: Arun Mahendra * Update prometheus/metric_test.go Co-authored-by: Bartlomiej Plotka * Update prometheus/metric.go Co-authored-by: Bartlomiej Plotka * updated comment and removed unnecessary test Co-authored-by: Bartlomiej Plotka --- prometheus/metric.go | 12 ++++++++++-- prometheus/metric_test.go | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/prometheus/metric.go b/prometheus/metric.go index 48d4a5d..63e1491 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -15,6 +15,7 @@ package prometheus import ( "errors" + "math" "sort" "strings" "time" @@ -184,8 +185,15 @@ func (m *withExemplarsMetric) Write(pb *dto.Metric) error { if i < len(pb.Histogram.Bucket) { pb.Histogram.Bucket[i].Exemplar = e } else { - // This is not possible as last bucket is Inf. - panic("no bucket was found for given exemplar value") + // The +Inf bucket should be explicitly added if there is an exemplar for it, similar to non-const histogram logic in https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L357-L365. + b := &dto.Bucket{ + CumulativeCount: proto.Uint64(pb.Histogram.Bucket[len(pb.Histogram.GetBucket())-1].GetCumulativeCount()), + UpperBound: proto.Float64(math.Inf(1)), + Exemplar: e, + } + pb.Histogram.Bucket = append(pb.Histogram.Bucket, b) + break + // Terminating the loop after creating the +Inf bucket and adding one exemplar, if there are other exemplars in the +Inf bucket range they will be ignored. } } default: diff --git a/prometheus/metric_test.go b/prometheus/metric_test.go index 61d807c..6e90d0b 100644 --- a/prometheus/metric_test.go +++ b/prometheus/metric_test.go @@ -14,6 +14,7 @@ package prometheus import ( + "math" "testing" //nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility. @@ -56,16 +57,19 @@ func TestWithExemplarsMetric(t *testing.T) { {Value: proto.Float64(89.0)}, {Value: proto.Float64(100.0)}, {Value: proto.Float64(157.0)}, + {Value: proto.Float64(500.0)}, + {Value: proto.Float64(2000.0)}, }} metric := dto.Metric{} if err := m.Write(&metric); err != nil { t.Fatal(err) } - if want, got := 4, len(metric.GetHistogram().Bucket); want != got { + if want, got := 5, len(metric.GetHistogram().Bucket); want != got { t.Errorf("want %v, got %v", want, got) } - expectedExemplarVals := []float64{24.0, 42.0, 100.0, 157.0} + // When there are more exemplars than there are buckets, a +Inf bucket will be created and the last exemplar value will be added. + expectedExemplarVals := []float64{24.0, 42.0, 100.0, 157.0, 500.0} for i, b := range metric.GetHistogram().Bucket { if b.Exemplar == nil { t.Errorf("Expected exemplar for bucket %v, got nil", i) @@ -74,5 +78,11 @@ func TestWithExemplarsMetric(t *testing.T) { t.Errorf("%v: want %v, got %v", i, want, got) } } + + infBucket := metric.GetHistogram().Bucket[len(metric.GetHistogram().Bucket)-1].GetUpperBound() + + if infBucket != math.Inf(1) { + t.Errorf("want %v, got %v", math.Inf(1), infBucket) + } }) } From 44ce5e1ee5015df547c585135d683fbab114dc82 Mon Sep 17 00:00:00 2001 From: Joseph Woodward Date: Tue, 2 Aug 2022 10:24:17 +0100 Subject: [PATCH 08/20] Ensure tests verify request params (#1047) * Ensure tests verify request params Signed-off-by: Joseph Woodward * Fix error message Signed-off-by: Joseph Woodward * gofumpt-ed with extra. Signed-off-by: bwplotka Co-authored-by: bwplotka --- api/prometheus/v1/api_test.go | 196 +++++++++++++++++++++++----------- 1 file changed, 134 insertions(+), 62 deletions(-) diff --git a/api/prometheus/v1/api_test.go b/api/prometheus/v1/api_test.go index 009035d..717a0f9 100644 --- a/api/prometheus/v1/api_test.go +++ b/api/prometheus/v1/api_test.go @@ -64,7 +64,7 @@ func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL { return u } -func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, Warnings, error) { +func (c *apiTestClient) Do(_ context.Context, req *http.Request) (*http.Response, []byte, Warnings, error) { test := c.curTest if req.URL.Path != test.reqPath { @@ -74,6 +74,25 @@ func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Respon c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method) } + var vals url.Values + switch test.reqMethod { + case http.MethodGet: + if req.URL.RawQuery != "" { + vals = req.URL.Query() + } + case http.MethodPost: + if req.Body != nil { + reqBody, _ := io.ReadAll(req.Body) + vals, _ = url.ParseQuery(string(reqBody)) + } else if req.URL.RawQuery != "" { + vals = req.URL.Query() + } + } + + if !reflect.DeepEqual(vals, test.reqParam) { + c.Fatalf("unexpected request parameters: want %s, got %s", vals, test.reqParam) + } + b, err := json.Marshal(test.inRes) if err != nil { c.Fatal(err) @@ -156,15 +175,15 @@ func TestAPIs(t *testing.T) { } } - doLabelNames := func(matches []string) func() (interface{}, Warnings, error) { + doLabelNames := func(matches []string, startTime, endTime time.Time) func() (interface{}, Warnings, error) { return func() (interface{}, Warnings, error) { - return promAPI.LabelNames(context.Background(), matches, time.Now().Add(-100*time.Hour), time.Now()) + return promAPI.LabelNames(context.Background(), matches, startTime, endTime) } } - doLabelValues := func(matches []string, label string) func() (interface{}, Warnings, error) { + doLabelValues := func(matches []string, label string, startTime, endTime time.Time) func() (interface{}, Warnings, error) { return func() (interface{}, Warnings, error) { - return promAPI.LabelValues(context.Background(), label, matches, time.Now().Add(-100*time.Hour), time.Now()) + return promAPI.LabelValues(context.Background(), label, matches, startTime, endTime) } } @@ -257,7 +276,7 @@ func TestAPIs(t *testing.T) { reqPath: "/api/v1/query", reqParam: url.Values{ "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, + "time": []string{formatTime(testTime)}, "timeout": []string{(5 * time.Second).String()}, }, res: &model.Scalar{ @@ -273,7 +292,7 @@ func TestAPIs(t *testing.T) { reqPath: "/api/v1/query", reqParam: url.Values{ "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, + "time": []string{formatTime(testTime)}, }, err: fmt.Errorf("some error"), }, @@ -291,7 +310,7 @@ func TestAPIs(t *testing.T) { reqPath: "/api/v1/query", reqParam: url.Values{ "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, + "time": []string{formatTime(testTime)}, }, err: errors.New("server_error: server error: 500"), }, @@ -309,7 +328,7 @@ func TestAPIs(t *testing.T) { reqPath: "/api/v1/query", reqParam: url.Values{ "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, + "time": []string{formatTime(testTime)}, }, err: errors.New("client_error: client error: 404"), }, @@ -329,7 +348,7 @@ func TestAPIs(t *testing.T) { reqPath: "/api/v1/query", reqParam: url.Values{ "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, + "time": []string{formatTime(testTime)}, }, res: &model.Scalar{ Value: 2, @@ -353,7 +372,7 @@ func TestAPIs(t *testing.T) { reqPath: "/api/v1/query", reqParam: url.Values{ "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, + "time": []string{formatTime(testTime)}, }, err: errors.New("client_error: client error: 404"), warnings: []string{"warning"}, @@ -363,7 +382,7 @@ func TestAPIs(t *testing.T) { do: doQueryRange("2", Range{ Start: testTime.Add(-time.Minute), End: testTime, - Step: time.Minute, + Step: 1 * time.Minute, }, WithTimeout(5*time.Second)), inErr: fmt.Errorf("some error"), @@ -371,96 +390,136 @@ func TestAPIs(t *testing.T) { reqPath: "/api/v1/query_range", reqParam: url.Values{ "query": []string{"2"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, - "step": []string{time.Minute.String()}, + "start": []string{formatTime(testTime.Add(-time.Minute))}, + "end": []string{formatTime(testTime)}, + "step": []string{"60"}, "timeout": []string{(5 * time.Second).String()}, }, err: fmt.Errorf("some error"), }, { - do: doLabelNames(nil), + do: doLabelNames(nil, testTime.Add(-100*time.Hour), testTime), inRes: []string{"val1", "val2"}, reqMethod: "GET", reqPath: "/api/v1/labels", - res: []string{"val1", "val2"}, + reqParam: url.Values{ + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + res: []string{"val1", "val2"}, }, { - do: doLabelNames(nil), + do: doLabelNames(nil, testTime.Add(-100*time.Hour), testTime), inRes: []string{"val1", "val2"}, inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/labels", - res: []string{"val1", "val2"}, - warnings: []string{"a"}, + reqParam: url.Values{ + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + res: []string{"val1", "val2"}, + warnings: []string{"a"}, }, { - do: doLabelNames(nil), + do: doLabelNames(nil, testTime.Add(-100*time.Hour), testTime), inErr: fmt.Errorf("some error"), reqMethod: "GET", reqPath: "/api/v1/labels", - err: fmt.Errorf("some error"), + reqParam: url.Values{ + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + err: fmt.Errorf("some error"), }, { - do: doLabelNames(nil), + do: doLabelNames(nil, testTime.Add(-100*time.Hour), testTime), inErr: fmt.Errorf("some error"), inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/labels", - err: fmt.Errorf("some error"), - warnings: []string{"a"}, + reqParam: url.Values{ + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + err: fmt.Errorf("some error"), + warnings: []string{"a"}, }, { - do: doLabelNames([]string{"up"}), + do: doLabelNames([]string{"up"}, testTime.Add(-100*time.Hour), testTime), inRes: []string{"val1", "val2"}, reqMethod: "GET", reqPath: "/api/v1/labels", - reqParam: url.Values{"match[]": {"up"}}, - res: []string{"val1", "val2"}, + reqParam: url.Values{ + "match[]": {"up"}, + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + res: []string{"val1", "val2"}, }, { - do: doLabelValues(nil, "mylabel"), + do: doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime), inRes: []string{"val1", "val2"}, reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - res: model.LabelValues{"val1", "val2"}, + reqParam: url.Values{ + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + res: model.LabelValues{"val1", "val2"}, }, { - do: doLabelValues(nil, "mylabel"), + do: doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime), inRes: []string{"val1", "val2"}, inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - res: model.LabelValues{"val1", "val2"}, - warnings: []string{"a"}, + reqParam: url.Values{ + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + res: model.LabelValues{"val1", "val2"}, + warnings: []string{"a"}, }, { - do: doLabelValues(nil, "mylabel"), + do: doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime), inErr: fmt.Errorf("some error"), reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - err: fmt.Errorf("some error"), + reqParam: url.Values{ + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + err: fmt.Errorf("some error"), }, { - do: doLabelValues(nil, "mylabel"), + do: doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime), inErr: fmt.Errorf("some error"), inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - err: fmt.Errorf("some error"), - warnings: []string{"a"}, + reqParam: url.Values{ + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + err: fmt.Errorf("some error"), + warnings: []string{"a"}, }, { - do: doLabelValues([]string{"up"}, "mylabel"), + do: doLabelValues([]string{"up"}, "mylabel", testTime.Add(-100*time.Hour), testTime), inRes: []string{"val1", "val2"}, reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - reqParam: url.Values{"match[]": {"up"}}, - res: model.LabelValues{"val1", "val2"}, + reqParam: url.Values{ + "match[]": {"up"}, + "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, + "end": []string{formatTime(testTime)}, + }, + res: model.LabelValues{"val1", "val2"}, }, { @@ -475,9 +534,9 @@ func TestAPIs(t *testing.T) { reqMethod: "GET", reqPath: "/api/v1/series", reqParam: url.Values{ - "match": []string{"up"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, + "match[]": []string{"up"}, + "start": []string{formatTime(testTime.Add(-time.Minute))}, + "end": []string{formatTime(testTime)}, }, res: []model.LabelSet{ { @@ -501,9 +560,9 @@ func TestAPIs(t *testing.T) { reqMethod: "GET", reqPath: "/api/v1/series", reqParam: url.Values{ - "match": []string{"up"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, + "match[]": []string{"up"}, + "start": []string{formatTime(testTime.Add(-time.Minute))}, + "end": []string{formatTime(testTime)}, }, res: []model.LabelSet{ { @@ -521,9 +580,9 @@ func TestAPIs(t *testing.T) { reqMethod: "GET", reqPath: "/api/v1/series", reqParam: url.Values{ - "match": []string{"up"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, + "match[]": []string{"up"}, + "start": []string{formatTime(testTime.Add(-time.Minute))}, + "end": []string{formatTime(testTime)}, }, err: fmt.Errorf("some error"), }, @@ -535,9 +594,9 @@ func TestAPIs(t *testing.T) { reqMethod: "GET", reqPath: "/api/v1/series", reqParam: url.Values{ - "match": []string{"up"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, + "match[]": []string{"up"}, + "start": []string{formatTime(testTime.Add(-time.Minute))}, + "end": []string{formatTime(testTime)}, }, err: fmt.Errorf("some error"), warnings: []string{"a"}, @@ -563,7 +622,10 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "POST", reqPath: "/api/v1/admin/tsdb/snapshot", - err: fmt.Errorf("some error"), + reqParam: url.Values{ + "skip_head": []string{"true"}, + }, + err: fmt.Errorf("some error"), }, { @@ -592,9 +654,9 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/admin/tsdb/delete_series", reqParam: url.Values{ - "match": []string{"up"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, + "match[]": []string{"up"}, + "start": []string{formatTime(testTime.Add(-time.Minute))}, + "end": []string{formatTime(testTime)}, }, }, @@ -604,9 +666,9 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/admin/tsdb/delete_series", reqParam: url.Values{ - "match": []string{"up"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, + "match[]": []string{"up"}, + "start": []string{formatTime(testTime.Add(-time.Minute))}, + "end": []string{formatTime(testTime)}, }, err: fmt.Errorf("some error"), }, @@ -1246,14 +1308,24 @@ func TestAPIs(t *testing.T) { do: doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime), reqMethod: "GET", reqPath: "/api/v1/query_exemplars", - inErr: fmt.Errorf("some error"), - err: fmt.Errorf("some error"), + reqParam: url.Values{ + "query": []string{"tns_request_duration_seconds_bucket"}, + "start": []string{formatTime(testTime.Add(-1 * time.Minute))}, + "end": []string{formatTime(testTime)}, + }, + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), }, { do: doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime), reqMethod: "GET", reqPath: "/api/v1/query_exemplars", + reqParam: url.Values{ + "query": []string{"tns_request_duration_seconds_bucket"}, + "start": []string{formatTime(testTime.Add(-1 * time.Minute))}, + "end": []string{formatTime(testTime)}, + }, inRes: []interface{}{ map[string]interface{}{ "seriesLabels": map[string]interface{}{ From 3faf3bae7076031e56adc1f76d0c2d817410597e Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Tue, 2 Aug 2022 11:32:30 +0200 Subject: [PATCH 09/20] Fixed support for unordered input of exemplars. (#1100) Signed-off-by: bwplotka --- prometheus/metric.go | 2 -- prometheus/metric_test.go | 16 ++++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/prometheus/metric.go b/prometheus/metric.go index 63e1491..f0941f6 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -192,8 +192,6 @@ func (m *withExemplarsMetric) Write(pb *dto.Metric) error { Exemplar: e, } pb.Histogram.Bucket = append(pb.Histogram.Bucket, b) - break - // Terminating the loop after creating the +Inf bucket and adding one exemplar, if there are other exemplars in the +Inf bucket range they will be ignored. } } default: diff --git a/prometheus/metric_test.go b/prometheus/metric_test.go index 6e90d0b..2d69b08 100644 --- a/prometheus/metric_test.go +++ b/prometheus/metric_test.go @@ -47,18 +47,19 @@ func TestWithExemplarsMetric(t *testing.T) { h := MustNewConstHistogram( NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil), 4711, 403.34, + // Four buckets, but we expect five as the +Inf bucket will be created if we see value outside of those buckets. map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233}, ) m := &withExemplarsMetric{Metric: h, exemplars: []*dto.Exemplar{ + {Value: proto.Float64(2000.0)}, // Unordered exemplars. + {Value: proto.Float64(500.0)}, + {Value: proto.Float64(42.0)}, + {Value: proto.Float64(157.0)}, + {Value: proto.Float64(100.0)}, + {Value: proto.Float64(89.0)}, {Value: proto.Float64(24.0)}, {Value: proto.Float64(25.1)}, - {Value: proto.Float64(42.0)}, - {Value: proto.Float64(89.0)}, - {Value: proto.Float64(100.0)}, - {Value: proto.Float64(157.0)}, - {Value: proto.Float64(500.0)}, - {Value: proto.Float64(2000.0)}, }} metric := dto.Metric{} if err := m.Write(&metric); err != nil { @@ -68,8 +69,7 @@ func TestWithExemplarsMetric(t *testing.T) { t.Errorf("want %v, got %v", want, got) } - // When there are more exemplars than there are buckets, a +Inf bucket will be created and the last exemplar value will be added. - expectedExemplarVals := []float64{24.0, 42.0, 100.0, 157.0, 500.0} + expectedExemplarVals := []float64{24.0, 25.1, 89.0, 157.0, 500.0} for i, b := range metric.GetHistogram().Bucket { if b.Exemplar == nil { t.Errorf("Expected exemplar for bucket %v, got nil", i) From c7488be2e4081d3afa59e63e221587546c58badf Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Tue, 2 Aug 2022 19:33:08 +0200 Subject: [PATCH 10/20] Added exemplar support to http middleware. (#1055) * Added exemplar support to http middlewares. Signed-off-by: Bartlomiej Plotka * Small fix. Signed-off-by: Bartlomiej Plotka * Fixed test. Signed-off-by: Bartlomiej Plotka * Added tests and options for RT. Signed-off-by: bwplotka * goimports. Signed-off-by: bwplotka --- prometheus/promhttp/instrument_client.go | 39 ++++-- prometheus/promhttp/instrument_client_test.go | 116 ++++++++++++++++-- prometheus/promhttp/instrument_server.go | 115 ++++++++++++----- prometheus/promhttp/instrument_server_test.go | 41 +++++-- prometheus/promhttp/option.go | 39 +++++- 5 files changed, 274 insertions(+), 76 deletions(-) diff --git a/prometheus/promhttp/instrument_client.go b/prometheus/promhttp/instrument_client.go index 861b4d2..097aff2 100644 --- a/prometheus/promhttp/instrument_client.go +++ b/prometheus/promhttp/instrument_client.go @@ -38,11 +38,11 @@ func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { // // See the example for ExampleInstrumentRoundTripperDuration for example usage. func InstrumentRoundTripperInFlight(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc { - return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + return func(r *http.Request) (*http.Response, error) { gauge.Inc() defer gauge.Dec() return next.RoundTrip(r) - }) + } } // InstrumentRoundTripperCounter is a middleware that wraps the provided @@ -59,22 +59,29 @@ func InstrumentRoundTripperInFlight(gauge prometheus.Gauge, next http.RoundTripp // If the wrapped RoundTripper panics or returns a non-nil error, the Counter // is not incremented. // +// Use with WithExemplarFromContext to instrument the exemplars on the counter of requests. +// // See the example for ExampleInstrumentRoundTripperDuration for example usage. func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.RoundTripper, opts ...Option) RoundTripperFunc { - rtOpts := &option{} + rtOpts := defaultOptions() for _, o := range opts { - o(rtOpts) + o.apply(rtOpts) } code, method := checkLabels(counter) - return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + return func(r *http.Request) (*http.Response, error) { resp, err := next.RoundTrip(r) if err == nil { + exemplarAdd( + counter.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)), + 1, + rtOpts.getExemplarFn(r.Context()), + ) counter.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)).Inc() } return resp, err - }) + } } // InstrumentRoundTripperDuration is a middleware that wraps the provided @@ -94,24 +101,30 @@ func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.Rou // If the wrapped RoundTripper panics or returns a non-nil error, no values are // reported. // +// Use with WithExemplarFromContext to instrument the exemplars on the duration histograms. +// // Note that this method is only guaranteed to never observe negative durations // if used with Go1.9+. func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundTripper, opts ...Option) RoundTripperFunc { - rtOpts := &option{} + rtOpts := defaultOptions() for _, o := range opts { - o(rtOpts) + o.apply(rtOpts) } code, method := checkLabels(obs) - return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + return func(r *http.Request) (*http.Response, error) { start := time.Now() resp, err := next.RoundTrip(r) if err == nil { - obs.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)).Observe(time.Since(start).Seconds()) + exemplarObserve( + obs.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)), + time.Since(start).Seconds(), + rtOpts.getExemplarFn(r.Context()), + ) } return resp, err - }) + } } // InstrumentTrace is used to offer flexibility in instrumenting the available @@ -149,7 +162,7 @@ type InstrumentTrace struct { // // See the example for ExampleInstrumentRoundTripperDuration for example usage. func InstrumentRoundTripperTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc { - return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + return func(r *http.Request) (*http.Response, error) { start := time.Now() trace := &httptrace.ClientTrace{ @@ -231,5 +244,5 @@ func InstrumentRoundTripperTrace(it *InstrumentTrace, next http.RoundTripper) Ro r = r.WithContext(httptrace.WithClientTrace(r.Context(), trace)) return next.RoundTrip(r) - }) + } } diff --git a/prometheus/promhttp/instrument_client_test.go b/prometheus/promhttp/instrument_client_test.go index aab8dbe..98667e8 100644 --- a/prometheus/promhttp/instrument_client_test.go +++ b/prometheus/promhttp/instrument_client_test.go @@ -18,14 +18,19 @@ import ( "log" "net/http" "net/http/httptest" + "reflect" + "sort" "strings" "testing" "time" "github.com/prometheus/client_golang/prometheus" + + dto "github.com/prometheus/client_model/go" + "google.golang.org/protobuf/proto" ) -func makeInstrumentedClient() (*http.Client, *prometheus.Registry) { +func makeInstrumentedClient(opts ...Option) (*http.Client, *prometheus.Registry) { client := http.DefaultClient client.Timeout = 1 * time.Second @@ -91,13 +96,91 @@ func makeInstrumentedClient() (*http.Client, *prometheus.Registry) { client.Transport = InstrumentRoundTripperInFlight(inFlightGauge, InstrumentRoundTripperCounter(counter, InstrumentRoundTripperTrace(trace, - InstrumentRoundTripperDuration(histVec, http.DefaultTransport), + InstrumentRoundTripperDuration(histVec, http.DefaultTransport, opts...), ), - ), + opts...), ) return client, reg } +func labelsToLabelPair(l prometheus.Labels) []*dto.LabelPair { + ret := make([]*dto.LabelPair, 0, len(l)) + for k, v := range l { + ret = append(ret, &dto.LabelPair{Name: proto.String(k), Value: proto.String(v)}) + } + sort.Slice(ret, func(i, j int) bool { + return *ret[i].Name < *ret[j].Name + }) + return ret +} + +func assetMetricAndExemplars( + t *testing.T, + reg *prometheus.Registry, + expectedNumMetrics int, + expectedExemplar []*dto.LabelPair, +) { + t.Helper() + + mfs, err := reg.Gather() + if err != nil { + t.Fatal(err) + } + if want, got := expectedNumMetrics, len(mfs); want != got { + t.Fatalf("unexpected number of metric families gathered, want %d, got %d", want, got) + } + + for _, mf := range mfs { + if len(mf.Metric) == 0 { + t.Errorf("metric family %s must not be empty", mf.GetName()) + } + for _, m := range mf.GetMetric() { + if c := m.GetCounter(); c != nil { + if len(expectedExemplar) == 0 { + if c.Exemplar != nil { + t.Errorf("expected no exemplar on the counter %v%v, got %v", mf.GetName(), m.Label, c.Exemplar.String()) + } + continue + } + + if c.Exemplar == nil { + t.Errorf("expected exemplar %v on the counter %v%v, got none", expectedExemplar, mf.GetName(), m.Label) + continue + } + if got := c.Exemplar.Label; !reflect.DeepEqual(expectedExemplar, got) { + t.Errorf("expected exemplar %v on the counter %v%v, got %v", expectedExemplar, mf.GetName(), m.Label, got) + } + continue + } + if h := m.GetHistogram(); h != nil { + found := false + for _, b := range h.GetBucket() { + if len(expectedExemplar) == 0 { + if b.Exemplar != nil { + t.Errorf("expected no exemplar on histogram %v%v bkt %v, got %v", mf.GetName(), m.Label, b.GetUpperBound(), b.Exemplar.String()) + } + continue + } + + if b.Exemplar == nil { + continue + } + if got := b.Exemplar.Label; !reflect.DeepEqual(expectedExemplar, got) { + t.Errorf("expected exemplar %v on the histogram %v%v on bkt %v, got %v", expectedExemplar, mf.GetName(), m.Label, b.GetUpperBound(), got) + continue + } + found = true + break + } + + if len(expectedExemplar) > 0 && !found { + t.Errorf("expected exemplar %v on at least one bucket of the histogram %v%v, got none", expectedExemplar, mf.GetName(), m.Label) + } + } + } + } +} + func TestClientMiddlewareAPI(t *testing.T) { client, reg := makeInstrumentedClient() backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -111,21 +194,28 @@ func TestClientMiddlewareAPI(t *testing.T) { } defer resp.Body.Close() - mfs, err := reg.Gather() + assetMetricAndExemplars(t, reg, 3, nil) +} + +func TestClientMiddlewareAPI_WithExemplars(t *testing.T) { + exemplar := prometheus.Labels{"traceID": "example situation observed by this metric"} + + client, reg := makeInstrumentedClient(WithExemplarFromContext(func(_ context.Context) prometheus.Labels { return exemplar })) + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer backend.Close() + + resp, err := client.Get(backend.URL) if err != nil { t.Fatal(err) } - if want, got := 3, len(mfs); want != got { - t.Fatalf("unexpected number of metric families gathered, want %d, got %d", want, got) - } - for _, mf := range mfs { - if len(mf.Metric) == 0 { - t.Errorf("metric family %s must not be empty", mf.GetName()) - } - } + defer resp.Body.Close() + + assetMetricAndExemplars(t, reg, 3, labelsToLabelPair(exemplar)) } -func TestClientMiddlewareAPIWithRequestContext(t *testing.T) { +func TestClientMiddlewareAPI_WithRequestContext(t *testing.T) { client, reg := makeInstrumentedClient() backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) diff --git a/prometheus/promhttp/instrument_server.go b/prometheus/promhttp/instrument_server.go index 1397a1e..b35d093 100644 --- a/prometheus/promhttp/instrument_server.go +++ b/prometheus/promhttp/instrument_server.go @@ -28,6 +28,22 @@ import ( // magicString is used for the hacky label test in checkLabels. Remove once fixed. const magicString = "zZgWfBxLqvG8kc8IMv3POi2Bb0tZI3vAnBx+gBaFi9FyPzB/CzKUer1yufDa" +func exemplarObserve(obs prometheus.Observer, val float64, labels map[string]string) { + if labels == nil { + obs.Observe(val) + return + } + obs.(prometheus.ExemplarObserver).ObserveWithExemplar(val, labels) +} + +func exemplarAdd(obs prometheus.Counter, val float64, labels map[string]string) { + if labels == nil { + obs.Add(val) + return + } + obs.(prometheus.ExemplarAdder).AddWithExemplar(val, labels) +} + // InstrumentHandlerInFlight is a middleware that wraps the provided // http.Handler. It sets the provided prometheus.Gauge to the number of // requests currently handled by the wrapped http.Handler. @@ -62,28 +78,37 @@ func InstrumentHandlerInFlight(g prometheus.Gauge, next http.Handler) http.Handl // Note that this method is only guaranteed to never observe negative durations // if used with Go1.9+. func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, opts ...Option) http.HandlerFunc { - mwOpts := &option{} + hOpts := defaultOptions() for _, o := range opts { - o(mwOpts) + o.apply(hOpts) } code, method := checkLabels(obs) if code { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { now := time.Now() d := newDelegator(w, nil) next.ServeHTTP(d, r) - obs.With(labels(code, method, r.Method, d.Status(), mwOpts.extraMethods...)).Observe(time.Since(now).Seconds()) - }) + exemplarObserve( + obs.With(labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)), + time.Since(now).Seconds(), + hOpts.getExemplarFn(r.Context()), + ) + } } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { now := time.Now() next.ServeHTTP(w, r) - obs.With(labels(code, method, r.Method, 0, mwOpts.extraMethods...)).Observe(time.Since(now).Seconds()) - }) + + exemplarObserve( + obs.With(labels(code, method, r.Method, 0, hOpts.extraMethods...)), + time.Since(now).Seconds(), + hOpts.getExemplarFn(r.Context()), + ) + } } // InstrumentHandlerCounter is a middleware that wraps the provided http.Handler @@ -104,25 +129,34 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler, opts ...Option) http.HandlerFunc { - mwOpts := &option{} + hOpts := defaultOptions() for _, o := range opts { - o(mwOpts) + o.apply(hOpts) } code, method := checkLabels(counter) if code { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w, nil) next.ServeHTTP(d, r) - counter.With(labels(code, method, r.Method, d.Status(), mwOpts.extraMethods...)).Inc() - }) + + exemplarAdd( + counter.With(labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)), + 1, + hOpts.getExemplarFn(r.Context()), + ) + } } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) - counter.With(labels(code, method, r.Method, 0, mwOpts.extraMethods...)).Inc() - }) + exemplarAdd( + counter.With(labels(code, method, r.Method, 0, hOpts.extraMethods...)), + 1, + hOpts.getExemplarFn(r.Context()), + ) + } } // InstrumentHandlerTimeToWriteHeader is a middleware that wraps the provided @@ -148,20 +182,24 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler, // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Handler, opts ...Option) http.HandlerFunc { - mwOpts := &option{} + hOpts := defaultOptions() for _, o := range opts { - o(mwOpts) + o.apply(hOpts) } code, method := checkLabels(obs) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { now := time.Now() d := newDelegator(w, func(status int) { - obs.With(labels(code, method, r.Method, status, mwOpts.extraMethods...)).Observe(time.Since(now).Seconds()) + exemplarObserve( + obs.With(labels(code, method, r.Method, status, hOpts.extraMethods...)), + time.Since(now).Seconds(), + hOpts.getExemplarFn(r.Context()), + ) }) next.ServeHTTP(d, r) - }) + } } // InstrumentHandlerRequestSize is a middleware that wraps the provided @@ -184,27 +222,34 @@ func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Ha // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler, opts ...Option) http.HandlerFunc { - mwOpts := &option{} + hOpts := defaultOptions() for _, o := range opts { - o(mwOpts) + o.apply(hOpts) } code, method := checkLabels(obs) - if code { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w, nil) next.ServeHTTP(d, r) size := computeApproximateRequestSize(r) - obs.With(labels(code, method, r.Method, d.Status(), mwOpts.extraMethods...)).Observe(float64(size)) - }) + exemplarObserve( + obs.With(labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)), + float64(size), + hOpts.getExemplarFn(r.Context()), + ) + } } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) size := computeApproximateRequestSize(r) - obs.With(labels(code, method, r.Method, 0, mwOpts.extraMethods...)).Observe(float64(size)) - }) + exemplarObserve( + obs.With(labels(code, method, r.Method, 0, hOpts.extraMethods...)), + float64(size), + hOpts.getExemplarFn(r.Context()), + ) + } } // InstrumentHandlerResponseSize is a middleware that wraps the provided @@ -227,9 +272,9 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler, // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler, opts ...Option) http.Handler { - mwOpts := &option{} + hOpts := defaultOptions() for _, o := range opts { - o(mwOpts) + o.apply(hOpts) } code, method := checkLabels(obs) @@ -237,7 +282,11 @@ func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w, nil) next.ServeHTTP(d, r) - obs.With(labels(code, method, r.Method, d.Status(), mwOpts.extraMethods...)).Observe(float64(d.Written())) + exemplarObserve( + obs.With(labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)), + float64(d.Written()), + hOpts.getExemplarFn(r.Context()), + ) }) } diff --git a/prometheus/promhttp/instrument_server_test.go b/prometheus/promhttp/instrument_server_test.go index a3720c2..4215348 100644 --- a/prometheus/promhttp/instrument_server_test.go +++ b/prometheus/promhttp/instrument_server_test.go @@ -14,6 +14,7 @@ package promhttp import ( + "context" "io" "log" "net/http" @@ -321,7 +322,7 @@ func TestLabels(t *testing.T) { } } -func TestMiddlewareAPI(t *testing.T) { +func makeInstrumentedHandler(handler http.HandlerFunc, opts ...Option) (http.Handler, *prometheus.Registry) { reg := prometheus.NewRegistry() inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{ @@ -366,25 +367,43 @@ func TestMiddlewareAPI(t *testing.T) { []string{}, ) - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("OK")) - }) - reg.MustRegister(inFlightGauge, counter, histVec, responseSize, writeHeaderVec) - chain := InstrumentHandlerInFlight(inFlightGauge, + return InstrumentHandlerInFlight(inFlightGauge, InstrumentHandlerCounter(counter, InstrumentHandlerDuration(histVec, InstrumentHandlerTimeToWriteHeader(writeHeaderVec, - InstrumentHandlerResponseSize(responseSize, handler), - ), - ), - ), - ) + InstrumentHandlerResponseSize(responseSize, handler, opts...), + opts...), + opts...), + opts...), + ), reg +} + +func TestMiddlewareAPI(t *testing.T) { + chain, reg := makeInstrumentedHandler(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("OK")) + }) r, _ := http.NewRequest("GET", "www.example.com", nil) w := httptest.NewRecorder() chain.ServeHTTP(w, r) + + assetMetricAndExemplars(t, reg, 5, nil) +} + +func TestMiddlewareAPI_WithExemplars(t *testing.T) { + exemplar := prometheus.Labels{"traceID": "example situation observed by this metric"} + + chain, reg := makeInstrumentedHandler(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("OK")) + }, WithExemplarFromContext(func(_ context.Context) prometheus.Labels { return exemplar })) + + r, _ := http.NewRequest("GET", "www.example.com", nil) + w := httptest.NewRecorder() + chain.ServeHTTP(w, r) + + assetMetricAndExemplars(t, reg, 5, labelsToLabelPair(exemplar)) } func TestInstrumentTimeToFirstWrite(t *testing.T) { diff --git a/prometheus/promhttp/option.go b/prometheus/promhttp/option.go index 35e41bd..c590d91 100644 --- a/prometheus/promhttp/option.go +++ b/prometheus/promhttp/option.go @@ -13,19 +13,46 @@ package promhttp -// Option are used to configure a middleware or round tripper.. -type Option func(*option) +import ( + "context" -type option struct { - extraMethods []string + "github.com/prometheus/client_golang/prometheus" +) + +// Option are used to configure both handler (middleware) or round tripper. +type Option interface { + apply(*options) } +// options store options for both a handler or round tripper. +type options struct { + extraMethods []string + getExemplarFn func(requestCtx context.Context) prometheus.Labels +} + +func defaultOptions() *options { + return &options{getExemplarFn: func(ctx context.Context) prometheus.Labels { return nil }} +} + +type optionApplyFunc func(*options) + +func (o optionApplyFunc) apply(opt *options) { o(opt) } + // WithExtraMethods adds additional HTTP methods to the list of allowed methods. // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods for the default list. // // See the example for ExampleInstrumentHandlerWithExtraMethods for example usage. func WithExtraMethods(methods ...string) Option { - return func(o *option) { + return optionApplyFunc(func(o *options) { o.extraMethods = methods - } + }) +} + +// WithExemplarFromContext adds allows to put a hook to all counter and histogram metrics. +// If the hook function returns non-nil labels, exemplars will be added for that request, otherwise metric +// will get instrumented without exemplar. +func WithExemplarFromContext(getExemplarFn func(requestCtx context.Context) prometheus.Labels) Option { + return optionApplyFunc(func(o *options) { + o.getExemplarFn = getExemplarFn + }) } From 618194de6ad3db637313666104533639011b470d Mon Sep 17 00:00:00 2001 From: Christoph Mewes Date: Wed, 3 Aug 2022 06:30:51 +0200 Subject: [PATCH 11/20] fix assorted oddities found by golangci-lint (#1040) * fix assorted oddities found by golangci-lint Signed-off-by: Christoph Mewes * permanently enable the linters Signed-off-by: Christoph Mewes * post-rebase blues Signed-off-by: Christoph Mewes --- .golangci.yml | 18 +- api/client.go | 2 +- api/prometheus/v1/api.go | 4 +- api/prometheus/v1/api_test.go | 231 +++--------------- prometheus/examples_test.go | 4 +- prometheus/internal/difflib.go | 2 +- prometheus/internal/difflib_test.go | 2 +- prometheus/internal/go_runtime_metrics.go | 4 +- prometheus/labels.go | 6 +- prometheus/process_collector.go | 4 +- prometheus/promhttp/http.go | 10 +- prometheus/promhttp/instrument_server.go | 2 +- prometheus/promhttp/instrument_server_test.go | 1 - prometheus/push/push_test.go | 5 +- prometheus/registry.go | 12 +- prometheus/registry_test.go | 29 +-- prometheus/testutil/lint.go | 4 +- prometheus/testutil/promlint/promlint.go | 3 +- prometheus/testutil/testutil.go | 14 +- 19 files changed, 111 insertions(+), 246 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index bedb09e..7e19530 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,10 +8,26 @@ output: linters: enable: + - deadcode + - depguard + - durationcheck + - errorlint + - exportloopref + - gofmt - gofumpt - goimports - - revive + - gosimple + - ineffassign - misspell + - nolintlint + - predeclared + - revive + - staticcheck + - structcheck + - unconvert + - unused + - varcheck + - wastedassign issues: max-same-issues: 0 diff --git a/api/client.go b/api/client.go index c91cf0c..72a0130 100644 --- a/api/client.go +++ b/api/client.go @@ -109,7 +109,7 @@ func (c *httpClient) URL(ep string, args map[string]string) *url.URL { for arg, val := range args { arg = ":" + arg - p = strings.Replace(p, arg, val, -1) + p = strings.ReplaceAll(p, arg, val) } u := *c.endpoint diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index bc89cb4..5f0ecef 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -856,7 +856,7 @@ func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time, opts .. } var qres queryResult - return model.Value(qres.v), warnings, json.Unmarshal(body, &qres) + return qres.v, warnings, json.Unmarshal(body, &qres) } func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range, opts ...Option) (model.Value, Warnings, error) { @@ -885,7 +885,7 @@ func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range, opts .. var qres queryResult - return model.Value(qres.v), warnings, json.Unmarshal(body, &qres) + return qres.v, warnings, json.Unmarshal(body, &qres) } func (h *httpAPI) Series(ctx context.Context, matches []string, startTime, endTime time.Time) ([]model.LabelSet, Warnings, error) { diff --git a/api/prometheus/v1/api_test.go b/api/prometheus/v1/api_test.go index 717a0f9..c933e0c 100644 --- a/api/prometheus/v1/api_test.go +++ b/api/prometheus/v1/api_test.go @@ -40,10 +40,8 @@ type apiTest struct { inRes interface{} reqPath string - reqParam url.Values reqMethod string res interface{} - warnings Warnings err error } @@ -55,7 +53,7 @@ type apiTestClient struct { func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL { path := ep for k, v := range args { - path = strings.Replace(path, ":"+k, v, -1) + path = strings.ReplaceAll(path, ":"+k, v) } u := &url.URL{ Host: "test:9090", @@ -74,25 +72,6 @@ func (c *apiTestClient) Do(_ context.Context, req *http.Request) (*http.Response c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method) } - var vals url.Values - switch test.reqMethod { - case http.MethodGet: - if req.URL.RawQuery != "" { - vals = req.URL.Query() - } - case http.MethodPost: - if req.Body != nil { - reqBody, _ := io.ReadAll(req.Body) - vals, _ = url.ParseQuery(string(reqBody)) - } else if req.URL.RawQuery != "" { - vals = req.URL.Query() - } - } - - if !reflect.DeepEqual(vals, test.reqParam) { - c.Fatalf("unexpected request parameters: want %s, got %s", vals, test.reqParam) - } - b, err := json.Marshal(test.inRes) if err != nil { c.Fatal(err) @@ -274,11 +253,6 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{formatTime(testTime)}, - "timeout": []string{(5 * time.Second).String()}, - }, res: &model.Scalar{ Value: 2, Timestamp: model.TimeFromUnix(testTime.Unix()), @@ -290,11 +264,7 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{formatTime(testTime)}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { do: doQuery("2", testTime), @@ -308,11 +278,7 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{formatTime(testTime)}, - }, - err: errors.New("server_error: server error: 500"), + err: errors.New("server_error: server error: 500"), }, { do: doQuery("2", testTime), @@ -326,11 +292,7 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{formatTime(testTime)}, - }, - err: errors.New("client_error: client error: 404"), + err: errors.New("client_error: client error: 404"), }, // Warning only. { @@ -346,15 +308,10 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{formatTime(testTime)}, - }, res: &model.Scalar{ Value: 2, Timestamp: model.TimeFromUnix(testTime.Unix()), }, - warnings: []string{"warning"}, }, // Warning + error. { @@ -370,12 +327,7 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{formatTime(testTime)}, - }, - err: errors.New("client_error: client error: 404"), - warnings: []string{"warning"}, + err: errors.New("client_error: client error: 404"), }, { @@ -388,14 +340,7 @@ func TestAPIs(t *testing.T) { reqMethod: "POST", reqPath: "/api/v1/query_range", - reqParam: url.Values{ - "query": []string{"2"}, - "start": []string{formatTime(testTime.Add(-time.Minute))}, - "end": []string{formatTime(testTime)}, - "step": []string{"60"}, - "timeout": []string{(5 * time.Second).String()}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { @@ -403,11 +348,7 @@ func TestAPIs(t *testing.T) { inRes: []string{"val1", "val2"}, reqMethod: "GET", reqPath: "/api/v1/labels", - reqParam: url.Values{ - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - res: []string{"val1", "val2"}, + res: []string{"val1", "val2"}, }, { do: doLabelNames(nil, testTime.Add(-100*time.Hour), testTime), @@ -415,12 +356,7 @@ func TestAPIs(t *testing.T) { inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/labels", - reqParam: url.Values{ - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - res: []string{"val1", "val2"}, - warnings: []string{"a"}, + res: []string{"val1", "val2"}, }, { @@ -428,11 +364,7 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "GET", reqPath: "/api/v1/labels", - reqParam: url.Values{ - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { do: doLabelNames(nil, testTime.Add(-100*time.Hour), testTime), @@ -440,24 +372,14 @@ func TestAPIs(t *testing.T) { inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/labels", - reqParam: url.Values{ - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - err: fmt.Errorf("some error"), - warnings: []string{"a"}, + err: errors.New("some error"), }, { do: doLabelNames([]string{"up"}, testTime.Add(-100*time.Hour), testTime), inRes: []string{"val1", "val2"}, reqMethod: "GET", reqPath: "/api/v1/labels", - reqParam: url.Values{ - "match[]": {"up"}, - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - res: []string{"val1", "val2"}, + res: []string{"val1", "val2"}, }, { @@ -465,11 +387,7 @@ func TestAPIs(t *testing.T) { inRes: []string{"val1", "val2"}, reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - reqParam: url.Values{ - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - res: model.LabelValues{"val1", "val2"}, + res: model.LabelValues{"val1", "val2"}, }, { do: doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime), @@ -477,12 +395,7 @@ func TestAPIs(t *testing.T) { inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - reqParam: url.Values{ - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - res: model.LabelValues{"val1", "val2"}, - warnings: []string{"a"}, + res: model.LabelValues{"val1", "val2"}, }, { @@ -490,11 +403,7 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - reqParam: url.Values{ - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { do: doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime), @@ -502,24 +411,14 @@ func TestAPIs(t *testing.T) { inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - reqParam: url.Values{ - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - err: fmt.Errorf("some error"), - warnings: []string{"a"}, + err: errors.New("some error"), }, { do: doLabelValues([]string{"up"}, "mylabel", testTime.Add(-100*time.Hour), testTime), inRes: []string{"val1", "val2"}, reqMethod: "GET", reqPath: "/api/v1/label/mylabel/values", - reqParam: url.Values{ - "match[]": {"up"}, - "start": []string{formatTime(testTime.Add(-100 * time.Hour))}, - "end": []string{formatTime(testTime)}, - }, - res: model.LabelValues{"val1", "val2"}, + res: model.LabelValues{"val1", "val2"}, }, { @@ -533,11 +432,6 @@ func TestAPIs(t *testing.T) { }, reqMethod: "GET", reqPath: "/api/v1/series", - reqParam: url.Values{ - "match[]": []string{"up"}, - "start": []string{formatTime(testTime.Add(-time.Minute))}, - "end": []string{formatTime(testTime)}, - }, res: []model.LabelSet{ { "__name__": "up", @@ -559,11 +453,6 @@ func TestAPIs(t *testing.T) { inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/series", - reqParam: url.Values{ - "match[]": []string{"up"}, - "start": []string{formatTime(testTime.Add(-time.Minute))}, - "end": []string{formatTime(testTime)}, - }, res: []model.LabelSet{ { "__name__": "up", @@ -571,7 +460,6 @@ func TestAPIs(t *testing.T) { "instance": "localhost:9090", }, }, - warnings: []string{"a"}, }, { @@ -579,12 +467,7 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "GET", reqPath: "/api/v1/series", - reqParam: url.Values{ - "match[]": []string{"up"}, - "start": []string{formatTime(testTime.Add(-time.Minute))}, - "end": []string{formatTime(testTime)}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, // Series with error and warning. { @@ -593,13 +476,7 @@ func TestAPIs(t *testing.T) { inWarnings: []string{"a"}, reqMethod: "GET", reqPath: "/api/v1/series", - reqParam: url.Values{ - "match[]": []string{"up"}, - "start": []string{formatTime(testTime.Add(-time.Minute))}, - "end": []string{formatTime(testTime)}, - }, - err: fmt.Errorf("some error"), - warnings: []string{"a"}, + err: errors.New("some error"), }, { @@ -609,9 +486,6 @@ func TestAPIs(t *testing.T) { }, reqMethod: "POST", reqPath: "/api/v1/admin/tsdb/snapshot", - reqParam: url.Values{ - "skip_head": []string{"true"}, - }, res: SnapshotResult{ Name: "20171210T211224Z-2be650b6d019eb54", }, @@ -622,10 +496,7 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "POST", reqPath: "/api/v1/admin/tsdb/snapshot", - reqParam: url.Values{ - "skip_head": []string{"true"}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { @@ -639,7 +510,7 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "POST", reqPath: "/api/v1/admin/tsdb/clean_tombstones", - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { @@ -653,11 +524,6 @@ func TestAPIs(t *testing.T) { }, reqMethod: "POST", reqPath: "/api/v1/admin/tsdb/delete_series", - reqParam: url.Values{ - "match[]": []string{"up"}, - "start": []string{formatTime(testTime.Add(-time.Minute))}, - "end": []string{formatTime(testTime)}, - }, }, { @@ -665,12 +531,7 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "POST", reqPath: "/api/v1/admin/tsdb/delete_series", - reqParam: url.Values{ - "match[]": []string{"up"}, - "start": []string{formatTime(testTime.Add(-time.Minute))}, - "end": []string{formatTime(testTime)}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { @@ -1129,11 +990,6 @@ func TestAPIs(t *testing.T) { }, reqMethod: "GET", reqPath: "/api/v1/targets/metadata", - reqParam: url.Values{ - "match_target": []string{"{job=\"prometheus\"}"}, - "metric": []string{"go_goroutines"}, - "limit": []string{"1"}, - }, res: []MetricMetadata{ { Target: map[string]string{ @@ -1152,12 +1008,7 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "GET", reqPath: "/api/v1/targets/metadata", - reqParam: url.Values{ - "match_target": []string{"{job=\"prometheus\"}"}, - "metric": []string{"go_goroutines"}, - "limit": []string{"1"}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { @@ -1173,10 +1024,6 @@ func TestAPIs(t *testing.T) { }, reqMethod: "GET", reqPath: "/api/v1/metadata", - reqParam: url.Values{ - "metric": []string{"go_goroutines"}, - "limit": []string{"1"}, - }, res: map[string][]Metadata{ "go_goroutines": { { @@ -1193,11 +1040,7 @@ func TestAPIs(t *testing.T) { inErr: fmt.Errorf("some error"), reqMethod: "GET", reqPath: "/api/v1/metadata", - reqParam: url.Values{ - "metric": []string{""}, - "limit": []string{"1"}, - }, - err: fmt.Errorf("some error"), + err: errors.New("some error"), }, { @@ -1308,24 +1151,14 @@ func TestAPIs(t *testing.T) { do: doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime), reqMethod: "GET", reqPath: "/api/v1/query_exemplars", - reqParam: url.Values{ - "query": []string{"tns_request_duration_seconds_bucket"}, - "start": []string{formatTime(testTime.Add(-1 * time.Minute))}, - "end": []string{formatTime(testTime)}, - }, - inErr: fmt.Errorf("some error"), - err: fmt.Errorf("some error"), + inErr: errors.New("some error"), + err: errors.New("some error"), }, { do: doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime), reqMethod: "GET", reqPath: "/api/v1/query_exemplars", - reqParam: url.Values{ - "query": []string{"tns_request_duration_seconds_bucket"}, - "start": []string{formatTime(testTime.Add(-1 * time.Minute))}, - "end": []string{formatTime(testTime)}, - }, inRes: []interface{}{ map[string]interface{}{ "seriesLabels": map[string]interface{}{ @@ -1395,7 +1228,9 @@ func TestAPIs(t *testing.T) { if err.Error() != test.err.Error() { t.Errorf("unexpected error: want %s, got %s", test.err, err) } - if apiErr, ok := err.(*Error); ok { + + apiErr := &Error{} + if ok := errors.As(err, &apiErr); ok { if apiErr.Detail != test.inRes { t.Errorf("%q should be %q", apiErr.Detail, test.inRes) } @@ -1620,9 +1455,13 @@ func TestAPIClientDo(t *testing.T) { } if test.expectedErr.Detail != "" { - apiErr := err.(*Error) - if apiErr.Detail != test.expectedErr.Detail { - t.Fatalf("expected error detail :%v, but got:%v", apiErr.Detail, test.expectedErr.Detail) + apiErr := &Error{} + if errors.As(err, &apiErr) { + if apiErr.Detail != test.expectedErr.Detail { + t.Fatalf("expected error detail :%v, but got:%v", apiErr.Detail, test.expectedErr.Detail) + } + } else { + t.Fatalf("expected v1.Error instance, but got:%T", err) } } diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index a7a8d0c..a868262 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -15,6 +15,7 @@ package prometheus_test import ( "bytes" + "errors" "fmt" "math" "net/http" @@ -713,7 +714,8 @@ func ExampleAlreadyRegisteredError() { Help: "The total number of requests served.", }) if err := prometheus.Register(reqCounter); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + are := &prometheus.AlreadyRegisteredError{} + if errors.As(err, are) { // A counter for that metric has been registered before. // Use the old counter from now on. reqCounter = are.ExistingCollector.(prometheus.Counter) diff --git a/prometheus/internal/difflib.go b/prometheus/internal/difflib.go index 12b2d70..fd45cad 100644 --- a/prometheus/internal/difflib.go +++ b/prometheus/internal/difflib.go @@ -485,7 +485,7 @@ func (m *SequenceMatcher) QuickRatio() float64 { if m.fullBCount == nil { m.fullBCount = map[string]int{} for _, s := range m.b { - m.fullBCount[s] = m.fullBCount[s] + 1 + m.fullBCount[s]++ } } diff --git a/prometheus/internal/difflib_test.go b/prometheus/internal/difflib_test.go index 820bbc6..e99c7cb 100644 --- a/prometheus/internal/difflib_test.go +++ b/prometheus/internal/difflib_test.go @@ -134,7 +134,7 @@ four` Context: 3, } result, _ := GetUnifiedDiffString(diff) - fmt.Println(strings.Replace(result, "\t", " ", -1)) + fmt.Println(strings.ReplaceAll(result, "\t", " ")) // Output: // --- Original 2005-01-26 23:30:50 // +++ Current 2010-04-02 10:20:52 diff --git a/prometheus/internal/go_runtime_metrics.go b/prometheus/internal/go_runtime_metrics.go index 6cbe063..97d17d6 100644 --- a/prometheus/internal/go_runtime_metrics.go +++ b/prometheus/internal/go_runtime_metrics.go @@ -61,9 +61,9 @@ func RuntimeMetricsToProm(d *metrics.Description) (string, string, string, bool) // name has - replaced with _ and is concatenated with the unit and // other data. name = strings.ReplaceAll(name, "-", "_") - name = name + "_" + unit + name += "_" + unit if d.Cumulative && d.Kind != metrics.KindFloat64Histogram { - name = name + "_total" + name += "_total" } valid := model.IsValidMetricName(model.LabelValue(namespace + "_" + subsystem + "_" + name)) diff --git a/prometheus/labels.go b/prometheus/labels.go index 2744443..6eee198 100644 --- a/prometheus/labels.go +++ b/prometheus/labels.go @@ -39,7 +39,7 @@ var errInconsistentCardinality = errors.New("inconsistent label cardinality") func makeInconsistentCardinalityError(fqName string, labels, labelValues []string) error { return fmt.Errorf( - "%s: %q has %d variable labels named %q but %d values %q were provided", + "%w: %q has %d variable labels named %q but %d values %q were provided", errInconsistentCardinality, fqName, len(labels), labels, len(labelValues), labelValues, @@ -49,7 +49,7 @@ func makeInconsistentCardinalityError(fqName string, labels, labelValues []strin func validateValuesInLabels(labels Labels, expectedNumberOfValues int) error { if len(labels) != expectedNumberOfValues { return fmt.Errorf( - "%s: expected %d label values but got %d in %#v", + "%w: expected %d label values but got %d in %#v", errInconsistentCardinality, expectedNumberOfValues, len(labels), labels, ) @@ -67,7 +67,7 @@ func validateValuesInLabels(labels Labels, expectedNumberOfValues int) error { func validateLabelValues(vals []string, expectedNumberOfValues int) error { if len(vals) != expectedNumberOfValues { return fmt.Errorf( - "%s: expected %d label values but got %d in %#v", + "%w: expected %d label values but got %d in %#v", errInconsistentCardinality, expectedNumberOfValues, len(vals), vals, ) diff --git a/prometheus/process_collector.go b/prometheus/process_collector.go index 1245428..9302725 100644 --- a/prometheus/process_collector.go +++ b/prometheus/process_collector.go @@ -153,11 +153,11 @@ func NewPidFileFn(pidFilePath string) func() (int, error) { return func() (int, error) { content, err := os.ReadFile(pidFilePath) if err != nil { - return 0, fmt.Errorf("can't read pid file %q: %+v", pidFilePath, err) + return 0, fmt.Errorf("can't read pid file %q: %w", pidFilePath, err) } pid, err := strconv.Atoi(strings.TrimSpace(string(content))) if err != nil { - return 0, fmt.Errorf("can't parse pid file %q: %+v", pidFilePath, err) + return 0, fmt.Errorf("can't parse pid file %q: %w", pidFilePath, err) } return pid, nil diff --git a/prometheus/promhttp/http.go b/prometheus/promhttp/http.go index a6e4f85..a4cc981 100644 --- a/prometheus/promhttp/http.go +++ b/prometheus/promhttp/http.go @@ -33,6 +33,7 @@ package promhttp import ( "compress/gzip" + "errors" "fmt" "io" "net/http" @@ -110,7 +111,8 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO errCnt.WithLabelValues("gathering") errCnt.WithLabelValues("encoding") if err := opts.Registry.Register(errCnt); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + are := &prometheus.AlreadyRegisteredError{} + if errors.As(err, are) { errCnt = are.ExistingCollector.(*prometheus.CounterVec) } else { panic(err) @@ -250,7 +252,8 @@ func InstrumentMetricHandler(reg prometheus.Registerer, handler http.Handler) ht cnt.WithLabelValues("500") cnt.WithLabelValues("503") if err := reg.Register(cnt); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + are := &prometheus.AlreadyRegisteredError{} + if errors.As(err, are) { cnt = are.ExistingCollector.(*prometheus.CounterVec) } else { panic(err) @@ -262,7 +265,8 @@ func InstrumentMetricHandler(reg prometheus.Registerer, handler http.Handler) ht Help: "Current number of scrapes being served.", }) if err := reg.Register(gge); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + are := &prometheus.AlreadyRegisteredError{} + if errors.As(err, are) { gge = are.ExistingCollector.(prometheus.Gauge) } else { panic(err) diff --git a/prometheus/promhttp/instrument_server.go b/prometheus/promhttp/instrument_server.go index b35d093..bfe5009 100644 --- a/prometheus/promhttp/instrument_server.go +++ b/prometheus/promhttp/instrument_server.go @@ -64,7 +64,7 @@ func InstrumentHandlerInFlight(g prometheus.Gauge, next http.Handler) http.Handl // names are "code" and "method". The function panics otherwise. For the "method" // label a predefined default label value set is used to filter given values. // Values besides predefined values will count as `unknown` method. -//`WithExtraMethods` can be used to add more methods to the set. The Observe +// `WithExtraMethods` can be used to add more methods to the set. The Observe // method of the Observer in the ObserverVec is called with the request duration // in seconds. Partitioning happens by HTTP status code and/or HTTP method if // the respective instance label names are present in the ObserverVec. For diff --git a/prometheus/promhttp/instrument_server_test.go b/prometheus/promhttp/instrument_server_test.go index 4215348..2a2cbf2 100644 --- a/prometheus/promhttp/instrument_server_test.go +++ b/prometheus/promhttp/instrument_server_test.go @@ -146,7 +146,6 @@ func TestLabelCheck(t *testing.T) { }, append(sc.varLabels, sc.curriedLabels...), )) - //nolint:typecheck // Ignore declared but unused error. for _, l := range sc.curriedLabels { c = c.MustCurryWith(prometheus.Labels{l: "dummy"}) o = o.MustCurryWith(prometheus.Labels{l: "dummy"}) diff --git a/prometheus/push/push_test.go b/prometheus/push/push_test.go index a0e8e9a..cc061cf 100644 --- a/prometheus/push/push_test.go +++ b/prometheus/push/push_test.go @@ -15,6 +15,7 @@ package push import ( "bytes" + "errors" "io" "net/http" "net/http/httptest" @@ -200,8 +201,8 @@ func TestPush(t *testing.T) { Push(); err == nil { t.Error("push with empty job succeeded") } else { - if got, want := err, errJobEmpty; got != want { - t.Errorf("got error %q, want %q", got, want) + if want := errJobEmpty; !errors.Is(err, want) { + t.Errorf("got error %q, want %q", err, want) } } diff --git a/prometheus/registry.go b/prometheus/registry.go index 36fd64e..325f665 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -15,6 +15,7 @@ package prometheus import ( "bytes" + "errors" "fmt" "os" "path/filepath" @@ -288,7 +289,7 @@ func (r *Registry) Register(c Collector) error { // Is the descriptor valid at all? if desc.err != nil { - return fmt.Errorf("descriptor %s is invalid: %s", desc, desc.err) + return fmt.Errorf("descriptor %s is invalid: %w", desc, desc.err) } // Is the descID unique? @@ -602,7 +603,7 @@ func processMetric( } dtoMetric := &dto.Metric{} if err := metric.Write(dtoMetric); err != nil { - return fmt.Errorf("error collecting metric %v: %s", desc, err) + return fmt.Errorf("error collecting metric %v: %w", desc, err) } metricFamily, ok := metricFamiliesByName[desc.fqName] if ok { // Existing name. @@ -724,12 +725,13 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { for i, g := range gs { mfs, err := g.Gather() if err != nil { - if multiErr, ok := err.(MultiError); ok { + multiErr := MultiError{} + if errors.As(err, &multiErr) { for _, err := range multiErr { - errs = append(errs, fmt.Errorf("[from Gatherer #%d] %s", i+1, err)) + errs = append(errs, fmt.Errorf("[from Gatherer #%d] %w", i+1, err)) } } else { - errs = append(errs, fmt.Errorf("[from Gatherer #%d] %s", i+1, err)) + errs = append(errs, fmt.Errorf("[from Gatherer #%d] %w", i+1, err)) } } for _, mf := range mfs { diff --git a/prometheus/registry_test.go b/prometheus/registry_test.go index 9443ef3..f0871ba 100644 --- a/prometheus/registry_test.go +++ b/prometheus/registry_test.go @@ -120,8 +120,8 @@ metric: < > `) - externalMetricFamilyAsProtoCompactText := []byte(`name:"externalname" help:"externaldocstring" type:COUNTER metric: label: counter: > -`) + externalMetricFamilyAsProtoCompactText := []byte(`name:"externalname" help:"externaldocstring" type:COUNTER metric: label: counter: >`) + externalMetricFamilyAsProtoCompactText = append(externalMetricFamilyAsProtoCompactText, []byte(" \n")...) expectedMetricFamily := &dto.MetricFamily{ Name: proto.String("name"), @@ -202,8 +202,8 @@ metric: < > `) - expectedMetricFamilyAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric: label: counter: > metric: label: counter: > -`) + expectedMetricFamilyAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric: label: counter: > metric: label: counter: >`) + expectedMetricFamilyAsProtoCompactText = append(expectedMetricFamilyAsProtoCompactText, []byte(" \n")...) externalMetricFamilyWithSameName := &dto.MetricFamily{ Name: proto.String("name"), @@ -228,8 +228,8 @@ metric: < }, } - expectedMetricFamilyMergedWithExternalAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric: label: counter: > metric: label: counter: > metric: label: counter: > -`) + expectedMetricFamilyMergedWithExternalAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric: label: counter: > metric: label: counter: > metric: label: counter: >`) + expectedMetricFamilyMergedWithExternalAsProtoCompactText = append(expectedMetricFamilyMergedWithExternalAsProtoCompactText, []byte(" \n")...) externalMetricFamilyWithInvalidLabelValue := &dto.MetricFamily{ Name: proto.String("name"), @@ -850,7 +850,8 @@ func TestAlreadyRegistered(t *testing.T) { if err = s.reRegisterWith(reg).Register(s.newCollector); err == nil { t.Fatal("expected error when registering new collector") } - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + are := &prometheus.AlreadyRegisteredError{} + if errors.As(err, are) { if are.ExistingCollector != s.originalCollector { t.Error("expected original collector but got something else") } @@ -931,7 +932,7 @@ func TestHistogramVecRegisterGatherConcurrency(t *testing.T) { return default: if err := reg.Register(hv); err != nil { - if _, ok := err.(prometheus.AlreadyRegisteredError); !ok { + if !errors.As(err, &prometheus.AlreadyRegisteredError{}) { t.Error("Registering failed:", err) } } @@ -1162,15 +1163,15 @@ func TestAlreadyRegisteredCollision(t *testing.T) { // Register should not fail, since each collector has a unique // set of sub-collectors, determined by their names and const label values. if err := reg.Register(&collector); err != nil { - alreadyRegErr, ok := err.(prometheus.AlreadyRegisteredError) - if !ok { + are := &prometheus.AlreadyRegisteredError{} + if !errors.As(err, are) { t.Fatal(err) } - previous := alreadyRegErr.ExistingCollector.(*collidingCollector) - current := alreadyRegErr.NewCollector.(*collidingCollector) + previous := are.ExistingCollector.(*collidingCollector) + current := are.NewCollector.(*collidingCollector) - t.Errorf("Unexpected registration error: %q\nprevious collector: %s (i=%d)\ncurrent collector %s (i=%d)", alreadyRegErr, previous.name, previous.i, current.name, current.i) + t.Errorf("Unexpected registration error: %q\nprevious collector: %s (i=%d)\ncurrent collector %s (i=%d)", are, previous.name, previous.i, current.name, current.i) } } } @@ -1240,7 +1241,7 @@ func TestNewMultiTRegistry(t *testing.T) { t.Run("two registries, one with error", func(t *testing.T) { m := prometheus.NewMultiTRegistry(prometheus.ToTransactionalGatherer(reg), treg) ret, done, err := m.Gather() - if err != treg.err { + if !errors.Is(err, treg.err) { t.Error("unexpected error:", err) } done() diff --git a/prometheus/testutil/lint.go b/prometheus/testutil/lint.go index 7681877..8d2f055 100644 --- a/prometheus/testutil/lint.go +++ b/prometheus/testutil/lint.go @@ -26,7 +26,7 @@ import ( func CollectAndLint(c prometheus.Collector, metricNames ...string) ([]promlint.Problem, error) { reg := prometheus.NewPedanticRegistry() if err := reg.Register(c); err != nil { - return nil, fmt.Errorf("registering collector failed: %s", err) + return nil, fmt.Errorf("registering collector failed: %w", err) } return GatherAndLint(reg, metricNames...) } @@ -37,7 +37,7 @@ func CollectAndLint(c prometheus.Collector, metricNames ...string) ([]promlint.P func GatherAndLint(g prometheus.Gatherer, metricNames ...string) ([]promlint.Problem, error) { got, err := g.Gather() if err != nil { - return nil, fmt.Errorf("gathering metrics failed: %s", err) + return nil, fmt.Errorf("gathering metrics failed: %w", err) } if metricNames != nil { got = filterMetrics(got, metricNames) diff --git a/prometheus/testutil/promlint/promlint.go b/prometheus/testutil/promlint/promlint.go index d534f55..a20f159 100644 --- a/prometheus/testutil/promlint/promlint.go +++ b/prometheus/testutil/promlint/promlint.go @@ -15,6 +15,7 @@ package promlint import ( + "errors" "fmt" "io" "regexp" @@ -83,7 +84,7 @@ func (l *Linter) Lint() ([]Problem, error) { mf := &dto.MetricFamily{} for { if err := d.Decode(mf); err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index 67d162c..cdf1802 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -126,7 +126,7 @@ func ToFloat64(c prometheus.Collector) float64 { 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)) + panic(fmt.Errorf("registering collector failed: %w", err)) } result, err := GatherAndCount(reg, metricNames...) if err != nil { @@ -142,7 +142,7 @@ func CollectAndCount(c prometheus.Collector, metricNames ...string) int { 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) + return 0, fmt.Errorf("gathering metrics failed: %w", err) } if metricNames != nil { got = filterMetrics(got, metricNames) @@ -161,7 +161,7 @@ func GatherAndCount(g prometheus.Gatherer, metricNames ...string) (int, error) { func CollectAndCompare(c prometheus.Collector, expected io.Reader, metricNames ...string) error { reg := prometheus.NewPedanticRegistry() if err := reg.Register(c); err != nil { - return fmt.Errorf("registering collector failed: %s", err) + return fmt.Errorf("registering collector failed: %w", err) } return GatherAndCompare(reg, expected, metricNames...) } @@ -182,7 +182,7 @@ func TransactionalGatherAndCompare(g prometheus.TransactionalGatherer, expected got, done, err := g.Gather() defer done() if err != nil { - return fmt.Errorf("gathering metrics failed: %s", err) + return fmt.Errorf("gathering metrics failed: %w", err) } if metricNames != nil { got = filterMetrics(got, metricNames) @@ -190,7 +190,7 @@ func TransactionalGatherAndCompare(g prometheus.TransactionalGatherer, expected var tp expfmt.TextParser wantRaw, err := tp.TextToMetricFamilies(expected) if err != nil { - return fmt.Errorf("parsing expected metrics failed: %s", err) + return fmt.Errorf("parsing expected metrics failed: %w", err) } want := internal.NormalizeMetricFamilies(wantRaw) @@ -206,13 +206,13 @@ func compare(got, want []*dto.MetricFamily) error { enc := expfmt.NewEncoder(&gotBuf, expfmt.FmtText) for _, mf := range got { if err := enc.Encode(mf); err != nil { - return fmt.Errorf("encoding gathered metrics failed: %s", err) + return fmt.Errorf("encoding gathered metrics failed: %w", err) } } enc = expfmt.NewEncoder(&wantBuf, expfmt.FmtText) for _, mf := range want { if err := enc.Encode(mf); err != nil { - return fmt.Errorf("encoding expected metrics failed: %s", err) + return fmt.Errorf("encoding expected metrics failed: %w", err) } } if diffErr := diff(wantBuf, gotBuf); diffErr != "" { From c576b951ad94df7cb97d1b3b6f5cdb49315f5721 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Fri, 5 Aug 2022 15:48:33 +0200 Subject: [PATCH 12/20] Generate new Go runtime metrics for go 1.19 (#1105) * Generate new Go runtime metrics Fix generation script Signed-off-by: Kemal Akkoyun * Address review issues Signed-off-by: Kemal Akkoyun --- .github/workflows/golangci-lint.yml | 2 +- .go-version | 1 + Makefile | 8 +++ prometheus/gen_go_collector_metrics_set.go | 57 +++++++++++-------- prometheus/go_collector_latest_test.go | 2 +- prometheus/go_collector_metrics_go119_test.go | 45 +++++++++++++++ 6 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 .go-version create mode 100644 prometheus/go_collector_metrics_go119_test.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 6034bcb..8f069d3 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -20,7 +20,7 @@ jobs: - name: install Go uses: actions/setup-go@v2 with: - go-version: 1.18.x + go-version-file: .go-version - name: Install snmp_exporter/generator dependencies run: sudo apt-get update && sudo apt-get -y install libsnmp-dev if: github.repository == 'prometheus/snmp_exporter' diff --git a/.go-version b/.go-version new file mode 100644 index 0000000..adc97d8 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.18 diff --git a/Makefile b/Makefile index f35cf58..4f526d7 100644 --- a/Makefile +++ b/Makefile @@ -18,3 +18,11 @@ test: deps common-test .PHONY: test-short test-short: deps common-test-short + +.PHONY: generate-go-collector-test-files +VERSIONS := 1.17 1.18 1.19 +generate-go-collector-test-files: + for GO_VERSION in $(VERSIONS); do \ + docker run --rm -v $(PWD):/workspace -w /workspace golang:$$GO_VERSION go run prometheus/gen_go_collector_metrics_set.go; \ + mv -f go_collector_metrics* prometheus; \ + done diff --git a/prometheus/gen_go_collector_metrics_set.go b/prometheus/gen_go_collector_metrics_set.go index 2f60ea3..74b67ac 100644 --- a/prometheus/gen_go_collector_metrics_set.go +++ b/prometheus/gen_go_collector_metrics_set.go @@ -25,28 +25,41 @@ import ( "os" "runtime" "runtime/metrics" - "strconv" "strings" "text/template" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/internal" + + "github.com/hashicorp/go-version" ) func main() { - if len(os.Args) != 2 { - log.Fatal("requires Go version (e.g. go1.17) as an argument") - } + var givenVersion string toolVersion := runtime.Version() - mtv := majorVersion(toolVersion) - mv := majorVersion(os.Args[1]) - if mtv != mv { - log.Fatalf("using Go version %q but expected Go version %q", mtv, mv) + if len(os.Args) != 2 { + log.Printf("requires Go version (e.g. go1.17) as an argument. Since it is not specified, assuming %s.", toolVersion) + givenVersion = toolVersion + } else { + givenVersion = os.Args[1] } - version, err := parseVersion(mv) + log.Printf("given version for Go: %s", givenVersion) + log.Printf("tool version for Go: %s", toolVersion) + + tv, err := version.NewVersion(strings.TrimPrefix(givenVersion, "go")) if err != nil { - log.Fatalf("parsing Go version: %v", err) + log.Fatal(err) } + gv, err := version.NewVersion(strings.TrimPrefix(toolVersion, "go")) + if err != nil { + log.Fatal(err) + } + if !gv.Equal(tv) { + log.Fatalf("using Go version %q but expected Go version %q", tv, gv) + } + + v := goVersion(gv.Segments()[1]) + log.Printf("generating metrics for Go version %q", v) // Generate code. var buf bytes.Buffer @@ -56,7 +69,7 @@ func main() { Cardinality int }{ Descriptions: metrics.All(), - GoVersion: version, + GoVersion: v, Cardinality: rmCardinality(), }) if err != nil { @@ -70,7 +83,7 @@ func main() { } // Write it to a file. - fname := fmt.Sprintf("go_collector_metrics_%s_test.go", version.Abbr()) + fname := fmt.Sprintf("go_collector_metrics_%s_test.go", v.Abbr()) if err := os.WriteFile(fname, result, 0o644); err != nil { log.Fatalf("writing file: %v", err) } @@ -86,19 +99,6 @@ func (g goVersion) Abbr() string { return fmt.Sprintf("go1%d", g) } -func parseVersion(s string) (goVersion, error) { - i := strings.IndexRune(s, '.') - if i < 0 { - return goVersion(-1), fmt.Errorf("bad Go version format") - } - i, err := strconv.Atoi(s[i+1:]) - return goVersion(i), err -} - -func majorVersion(v string) string { - return v[:strings.LastIndexByte(v, '.')] -} - func rmCardinality() int { cardinality := 0 @@ -123,6 +123,7 @@ func rmCardinality() int { name[strings.IndexRune(name, ':')+1:], ) cardinality += len(buckets) + 3 // Plus total count, sum, and the implicit infinity bucket. + // runtime/metrics bucket boundaries are lower-bound-inclusive, but // always represents each actual *boundary* so Buckets is always // 1 longer than Counts, while in Prometheus the mapping is one-to-one, @@ -134,6 +135,12 @@ func rmCardinality() int { // We already counted the infinity bucket separately. cardinality-- } + // Prometheus also doesn't have buckets for -Inf, so they need to be omitted. + // See the following PR for more information: + // https://github.com/prometheus/client_golang/pull/1049 + if buckets[0] == math.Inf(-1) { + cardinality-- + } } return cardinality diff --git a/prometheus/go_collector_latest_test.go b/prometheus/go_collector_latest_test.go index 11094c8..df18d5d 100644 --- a/prometheus/go_collector_latest_test.go +++ b/prometheus/go_collector_latest_test.go @@ -269,7 +269,7 @@ func TestMemStatsEquivalence(t *testing.T) { } func TestExpectedRuntimeMetrics(t *testing.T) { - goMetrics := collectGoMetrics(t, goRuntimeMetricsCollection) + goMetrics := collectGoMetrics(t, goRuntimeMemStatsCollection|goRuntimeMetricsCollection) goMetricSet := make(map[string]Metric) for _, m := range goMetrics { goMetricSet[m.Desc().fqName] = m diff --git a/prometheus/go_collector_metrics_go119_test.go b/prometheus/go_collector_metrics_go119_test.go new file mode 100644 index 0000000..ec3430a --- /dev/null +++ b/prometheus/go_collector_metrics_go119_test.go @@ -0,0 +1,45 @@ +// Code generated by gen_go_collector_metrics_set.go; DO NOT EDIT. +//go:generate go run gen_go_collector_metrics_set.go go1.19 + +//go:build go1.19 && !go1.20 +// +build go1.19,!go1.20 + +package prometheus + +var expectedRuntimeMetrics = map[string]string{ + "/cgo/go-to-c-calls:calls": "go_cgo_go_to_c_calls_calls_total", + "/gc/cycles/automatic:gc-cycles": "go_gc_cycles_automatic_gc_cycles_total", + "/gc/cycles/forced:gc-cycles": "go_gc_cycles_forced_gc_cycles_total", + "/gc/cycles/total:gc-cycles": "go_gc_cycles_total_gc_cycles_total", + "/gc/heap/allocs-by-size:bytes": "go_gc_heap_allocs_by_size_bytes", + "/gc/heap/allocs:bytes": "go_gc_heap_allocs_bytes_total", + "/gc/heap/allocs:objects": "go_gc_heap_allocs_objects_total", + "/gc/heap/frees-by-size:bytes": "go_gc_heap_frees_by_size_bytes", + "/gc/heap/frees:bytes": "go_gc_heap_frees_bytes_total", + "/gc/heap/frees:objects": "go_gc_heap_frees_objects_total", + "/gc/heap/goal:bytes": "go_gc_heap_goal_bytes", + "/gc/heap/objects:objects": "go_gc_heap_objects_objects", + "/gc/heap/tiny/allocs:objects": "go_gc_heap_tiny_allocs_objects_total", + "/gc/limiter/last-enabled:gc-cycle": "go_gc_limiter_last_enabled_gc_cycle", + "/gc/pauses:seconds": "go_gc_pauses_seconds", + "/gc/stack/starting-size:bytes": "go_gc_stack_starting_size_bytes", + "/memory/classes/heap/free:bytes": "go_memory_classes_heap_free_bytes", + "/memory/classes/heap/objects:bytes": "go_memory_classes_heap_objects_bytes", + "/memory/classes/heap/released:bytes": "go_memory_classes_heap_released_bytes", + "/memory/classes/heap/stacks:bytes": "go_memory_classes_heap_stacks_bytes", + "/memory/classes/heap/unused:bytes": "go_memory_classes_heap_unused_bytes", + "/memory/classes/metadata/mcache/free:bytes": "go_memory_classes_metadata_mcache_free_bytes", + "/memory/classes/metadata/mcache/inuse:bytes": "go_memory_classes_metadata_mcache_inuse_bytes", + "/memory/classes/metadata/mspan/free:bytes": "go_memory_classes_metadata_mspan_free_bytes", + "/memory/classes/metadata/mspan/inuse:bytes": "go_memory_classes_metadata_mspan_inuse_bytes", + "/memory/classes/metadata/other:bytes": "go_memory_classes_metadata_other_bytes", + "/memory/classes/os-stacks:bytes": "go_memory_classes_os_stacks_bytes", + "/memory/classes/other:bytes": "go_memory_classes_other_bytes", + "/memory/classes/profiling/buckets:bytes": "go_memory_classes_profiling_buckets_bytes", + "/memory/classes/total:bytes": "go_memory_classes_total_bytes", + "/sched/gomaxprocs:threads": "go_sched_gomaxprocs_threads", + "/sched/goroutines:goroutines": "go_sched_goroutines_goroutines", + "/sched/latencies:seconds": "go_sched_latencies_seconds", +} + +const expectedRuntimeMetricsCardinality = 81 From 1638da9ae43be6ff5ad522ca402712e26b4a19c7 Mon Sep 17 00:00:00 2001 From: Soroosh Azary Marhabi Date: Fri, 5 Aug 2022 18:57:47 +0430 Subject: [PATCH 13/20] testutil: Add ScrapeAndCompare (#1043) * testutil: Add ScrapeAndCompare Signed-off-by: sazary * testutil: Use %w verb wherever we're using an error in fmt.Errorf Signed-off-by: sazary * Format Signed-off-by: Kemal Akkoyun Co-authored-by: Kemal Akkoyun Co-authored-by: Kemal Akkoyun --- prometheus/testutil/testutil.go | 64 ++++++++++++++++++++++++---- prometheus/testutil/testutil_test.go | 58 +++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index cdf1802..91b83b5 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -41,12 +41,12 @@ import ( "bytes" "fmt" "io" + "net/http" "reflect" "github.com/davecgh/go-spew/spew" - "github.com/prometheus/common/expfmt" - dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/internal" @@ -155,6 +155,34 @@ func GatherAndCount(g prometheus.Gatherer, metricNames ...string) (int, error) { return result, nil } +// ScrapeAndCompare calls a remote exporter's endpoint which is expected to return some metrics in +// plain text format. Then it compares it with the results that the `expected` would return. +// If the `metricNames` is not empty it would filter the comparison only to the given metric names. +func ScrapeAndCompare(url string, expected io.Reader, metricNames ...string) error { + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("scraping metrics failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("the scraping target returned a status code other than 200: %d", + resp.StatusCode) + } + + scraped, err := convertReaderToMetricFamily(resp.Body) + if err != nil { + return err + } + + wanted, err := convertReaderToMetricFamily(expected) + if err != nil { + return err + } + + return compareMetricFamilies(scraped, wanted, metricNames...) +} + // CollectAndCompare registers the provided Collector with a newly created // pedantic Registry. It then calls GatherAndCompare with that Registry and with // the provided metricNames. @@ -184,17 +212,35 @@ func TransactionalGatherAndCompare(g prometheus.TransactionalGatherer, expected if err != nil { return fmt.Errorf("gathering metrics failed: %w", err) } + + wanted, err := convertReaderToMetricFamily(expected) + if err != nil { + return err + } + + return compareMetricFamilies(got, wanted, metricNames...) +} + +// convertReaderToMetricFamily would read from a io.Reader object and convert it to a slice of +// dto.MetricFamily. +func convertReaderToMetricFamily(reader io.Reader) ([]*dto.MetricFamily, error) { + var tp expfmt.TextParser + notNormalized, err := tp.TextToMetricFamilies(reader) + if err != nil { + return nil, fmt.Errorf("converting reader to metric families failed: %w", err) + } + + return internal.NormalizeMetricFamilies(notNormalized), nil +} + +// compareMetricFamilies would compare 2 slices of metric families, and optionally filters both of +// them to the `metricNames` provided. +func compareMetricFamilies(got, expected []*dto.MetricFamily, metricNames ...string) error { if metricNames != nil { got = filterMetrics(got, metricNames) } - var tp expfmt.TextParser - wantRaw, err := tp.TextToMetricFamilies(expected) - if err != nil { - return fmt.Errorf("parsing expected metrics failed: %w", err) - } - want := internal.NormalizeMetricFamilies(wantRaw) - return compare(got, want) + return compare(got, expected) } // compare encodes both provided slices of metric families into the text format, diff --git a/prometheus/testutil/testutil_test.go b/prometheus/testutil/testutil_test.go index 96a6f38..2a46e5d 100644 --- a/prometheus/testutil/testutil_test.go +++ b/prometheus/testutil/testutil_test.go @@ -14,6 +14,9 @@ package testutil import ( + "fmt" + "net/http" + "net/http/httptest" "strings" "testing" @@ -308,6 +311,61 @@ Diff: } } +func TestScrapeAndCompare(t *testing.T) { + const expected = ` + # HELP some_total A value that represents a counter. + # TYPE some_total counter + + some_total{ label1 = "value1" } 1 + ` + + expectedReader := strings.NewReader(expected) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, expected) + })) + defer ts.Close() + + if err := ScrapeAndCompare(ts.URL, expectedReader, "some_total"); err != nil { + t.Errorf("unexpected scraping result:\n%s", err) + } +} + +func TestScrapeAndCompareFetchingFail(t *testing.T) { + err := ScrapeAndCompare("some_url", strings.NewReader("some expectation"), "some_total") + if err == nil { + t.Errorf("expected an error but got nil") + } + if !strings.HasPrefix(err.Error(), "scraping metrics failed") { + t.Errorf("unexpected error happened: %s", err) + } +} + +func TestScrapeAndCompareBadStatusCode(t *testing.T) { + const expected = ` + # HELP some_total A value that represents a counter. + # TYPE some_total counter + + some_total{ label1 = "value1" } 1 + ` + + expectedReader := strings.NewReader(expected) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadGateway) + fmt.Fprintln(w, expected) + })) + defer ts.Close() + + err := ScrapeAndCompare(ts.URL, expectedReader, "some_total") + if err == nil { + t.Errorf("expected an error but got nil") + } + if !strings.HasPrefix(err.Error(), "the scraping target returned a status code other than 200") { + t.Errorf("unexpected error happened: %s", err) + } +} + func TestCollectAndCount(t *testing.T) { c := prometheus.NewCounterVec( prometheus.CounterOpts{ From d44fbbefdd19086a786a7e4a6e215bfc88468ec5 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Fri, 5 Aug 2022 07:28:54 -0700 Subject: [PATCH 14/20] Fix build against GopherJS (#897) * Fix build against GopherJS When building against GopherJS, ThreadCreateProfile and Getpid are not available. Return 1 to shim the functions. Signed-off-by: Christian Stewart * Fix formatting Signed-off-by: Kemal Akkoyun * Fix linter issue Move build tags for licence header checks Signed-off-by: Kemal Akkoyun Co-authored-by: Kemal Akkoyun --- prometheus/get_pid.go | 26 ++++++++++++++++++++++++++ prometheus/get_pid_gopherjs.go | 23 +++++++++++++++++++++++ prometheus/go_collector.go | 5 +++-- prometheus/num_threads.go | 25 +++++++++++++++++++++++++ prometheus/num_threads_gopherjs.go | 22 ++++++++++++++++++++++ prometheus/process_collector.go | 3 +-- prometheus/process_collector_js.go | 26 ++++++++++++++++++++++++++ prometheus/process_collector_other.go | 4 ++-- 8 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 prometheus/get_pid.go create mode 100644 prometheus/get_pid_gopherjs.go create mode 100644 prometheus/num_threads.go create mode 100644 prometheus/num_threads_gopherjs.go create mode 100644 prometheus/process_collector_js.go diff --git a/prometheus/get_pid.go b/prometheus/get_pid.go new file mode 100644 index 0000000..614fd61 --- /dev/null +++ b/prometheus/get_pid.go @@ -0,0 +1,26 @@ +// Copyright 2015 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. + +//go:build !js || wasm +// +build !js wasm + +package prometheus + +import "os" + +func getPIDFn() func() (int, error) { + pid := os.Getpid() + return func() (int, error) { + return pid, nil + } +} diff --git a/prometheus/get_pid_gopherjs.go b/prometheus/get_pid_gopherjs.go new file mode 100644 index 0000000..eaf8059 --- /dev/null +++ b/prometheus/get_pid_gopherjs.go @@ -0,0 +1,23 @@ +// Copyright 2015 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. + +//go:build js && !wasm +// +build js,!wasm + +package prometheus + +func getPIDFn() func() (int, error) { + return func() (int, error) { + return 1, nil + } +} diff --git a/prometheus/go_collector.go b/prometheus/go_collector.go index 4d792aa..5fd7300 100644 --- a/prometheus/go_collector.go +++ b/prometheus/go_collector.go @@ -246,8 +246,9 @@ func (c *baseGoCollector) Describe(ch chan<- *Desc) { // Collect returns the current state of all metrics of the collector. func (c *baseGoCollector) Collect(ch chan<- Metric) { ch <- MustNewConstMetric(c.goroutinesDesc, GaugeValue, float64(runtime.NumGoroutine())) - n, _ := runtime.ThreadCreateProfile(nil) - ch <- MustNewConstMetric(c.threadsDesc, GaugeValue, float64(n)) + + n := getRuntimeNumThreads() + ch <- MustNewConstMetric(c.threadsDesc, GaugeValue, n) var stats debug.GCStats stats.PauseQuantiles = make([]time.Duration, 5) diff --git a/prometheus/num_threads.go b/prometheus/num_threads.go new file mode 100644 index 0000000..7c12b21 --- /dev/null +++ b/prometheus/num_threads.go @@ -0,0 +1,25 @@ +// Copyright 2018 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. + +//go:build !js || wasm +// +build !js wasm + +package prometheus + +import "runtime" + +// getRuntimeNumThreads returns the number of open OS threads. +func getRuntimeNumThreads() float64 { + n, _ := runtime.ThreadCreateProfile(nil) + return float64(n) +} diff --git a/prometheus/num_threads_gopherjs.go b/prometheus/num_threads_gopherjs.go new file mode 100644 index 0000000..7348df0 --- /dev/null +++ b/prometheus/num_threads_gopherjs.go @@ -0,0 +1,22 @@ +// Copyright 2018 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. + +//go:build js && !wasm +// +build js,!wasm + +package prometheus + +// getRuntimeNumThreads returns the number of open OS threads. +func getRuntimeNumThreads() float64 { + return 1 +} diff --git a/prometheus/process_collector.go b/prometheus/process_collector.go index 9302725..8548dd1 100644 --- a/prometheus/process_collector.go +++ b/prometheus/process_collector.go @@ -103,8 +103,7 @@ func NewProcessCollector(opts ProcessCollectorOpts) Collector { } if opts.PidFn == nil { - pid := os.Getpid() - c.pidFn = func() (int, error) { return pid, nil } + c.pidFn = getPIDFn() } else { c.pidFn = opts.PidFn } diff --git a/prometheus/process_collector_js.go b/prometheus/process_collector_js.go new file mode 100644 index 0000000..b1e363d --- /dev/null +++ b/prometheus/process_collector_js.go @@ -0,0 +1,26 @@ +// Copyright 2019 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. + +//go:build js +// +build js + +package prometheus + +func canCollectProcess() bool { + return false +} + +func (c *processCollector) processCollect(ch chan<- Metric) { + // noop on this platform + return +} diff --git a/prometheus/process_collector_other.go b/prometheus/process_collector_other.go index 2dc3660..c0152cd 100644 --- a/prometheus/process_collector_other.go +++ b/prometheus/process_collector_other.go @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !windows -// +build !windows +//go:build !windows && !js +// +build !windows,!js package prometheus From 5b7e8b2e6716df0ceda9df81feb15910c7efa150 Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Fri, 5 Aug 2022 19:37:46 +0200 Subject: [PATCH 15/20] collectors.GoCollector: Added rule support for granular metric configuration. (#1102) * goCollector: Added rule support for granular metric configuration. Fixes: https://github.com/prometheus/client_golang/issues/1089 Signed-off-by: bwplotka * Added compatibility mode with old options. (#1107) * Added compatibility mode with old options. Signed-off-by: bwplotka * Copyright header. Signed-off-by: bwplotka * Remove bucket option for now. (#1108) Signed-off-by: bwplotka * collectors/GoCollector: Add tests and examples (#1109) * Add tests and examples Signed-off-by: Kemal Akkoyun * Add docs for the presets Signed-off-by: Kemal Akkoyun Co-authored-by: Kemal Akkoyun --- prometheus/collector.go | 6 +- prometheus/collectors/go_collector_latest.go | 186 +++++++++---- .../collectors/go_collector_latest_test.go | 263 +++++++++++++++++- prometheus/go_collector.go | 7 +- prometheus/go_collector_latest.go | 248 ++++++++++------- prometheus/go_collector_latest_test.go | 65 ++++- prometheus/internal/go_collector_options.go | 32 +++ 7 files changed, 626 insertions(+), 181 deletions(-) create mode 100644 prometheus/internal/go_collector_options.go diff --git a/prometheus/collector.go b/prometheus/collector.go index ac1ca3c..cf05079 100644 --- a/prometheus/collector.go +++ b/prometheus/collector.go @@ -69,9 +69,9 @@ type Collector interface { // If a Collector collects the same metrics throughout its lifetime, its // Describe method can simply be implemented as: // -// func (c customCollector) Describe(ch chan<- *Desc) { -// DescribeByCollect(c, ch) -// } +// func (c customCollector) Describe(ch chan<- *Desc) { +// DescribeByCollect(c, ch) +// } // // However, this will not work if the metrics collected change dynamically over // the lifetime of the Collector in a way that their combined set of descriptors diff --git a/prometheus/collectors/go_collector_latest.go b/prometheus/collectors/go_collector_latest.go index 9435e96..246c5ea 100644 --- a/prometheus/collectors/go_collector_latest.go +++ b/prometheus/collectors/go_collector_latest.go @@ -16,77 +16,145 @@ package collectors -import "github.com/prometheus/client_golang/prometheus" +import ( + "regexp" -//nolint:staticcheck // Ignore SA1019 until v2. -type goOptions = prometheus.GoCollectorOptions -type goOption func(o *goOptions) + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/internal" +) +var ( + // MetricsAll allows all the metrics to be collected from Go runtime. + MetricsAll = GoRuntimeMetricsRule{regexp.MustCompile("/.*")} + // MetricsGC allows only GC metrics to be collected from Go runtime. + // e.g. go_gc_cycles_automatic_gc_cycles_total + MetricsGC = GoRuntimeMetricsRule{regexp.MustCompile(`^/gc/.*`)} + // MetricsMemory allows only memory metrics to be collected from Go runtime. + // e.g. go_memory_classes_heap_free_bytes + MetricsMemory = GoRuntimeMetricsRule{regexp.MustCompile(`^/memory/.*`)} + // MetricsScheduler allows only scheduler metrics to be collected from Go runtime. + // e.g. go_sched_goroutines_goroutines + MetricsScheduler = GoRuntimeMetricsRule{regexp.MustCompile(`^/sched/.*`)} +) + +// WithGoCollectorMemStatsMetricsDisabled disables metrics that is gathered in runtime.MemStats structure such as: +// +// go_memstats_alloc_bytes +// go_memstats_alloc_bytes_total +// go_memstats_sys_bytes +// go_memstats_lookups_total +// go_memstats_mallocs_total +// go_memstats_frees_total +// go_memstats_heap_alloc_bytes +// go_memstats_heap_sys_bytes +// go_memstats_heap_idle_bytes +// go_memstats_heap_inuse_bytes +// go_memstats_heap_released_bytes +// go_memstats_heap_objects +// go_memstats_stack_inuse_bytes +// go_memstats_stack_sys_bytes +// go_memstats_mspan_inuse_bytes +// go_memstats_mspan_sys_bytes +// go_memstats_mcache_inuse_bytes +// go_memstats_mcache_sys_bytes +// go_memstats_buck_hash_sys_bytes +// go_memstats_gc_sys_bytes +// go_memstats_other_sys_bytes +// go_memstats_next_gc_bytes +// +// so the metrics known from pre client_golang v1.12.0, +// +// NOTE(bwplotka): The above represents runtime.MemStats statistics, but they are +// actually implemented using new runtime/metrics package. (except skipped go_memstats_gc_cpu_fraction +// -- see https://github.com/prometheus/client_golang/issues/842#issuecomment-861812034 for explanation). +// +// Some users might want to disable this on collector level (although you can use scrape relabelling on Prometheus), +// because similar metrics can be now obtained using WithGoCollectorRuntimeMetrics. Note that the semantics of new +// metrics might be different, plus the names can be change over time with different Go version. +// +// NOTE(bwplotka): Changing metric names can be tedious at times as the alerts, recording rules and dashboards have to be adjusted. +// The old metrics are also very useful, with many guides and books written about how to interpret them. +// +// As a result our recommendation would be to stick with MemStats like metrics and enable other runtime/metrics if you are interested +// in advanced insights Go provides. See ExampleGoCollector_WithAdvancedGoMetrics. +func WithGoCollectorMemStatsMetricsDisabled() func(options *internal.GoCollectorOptions) { + return func(o *internal.GoCollectorOptions) { + o.DisableMemStatsLikeMetrics = true + } +} + +// GoRuntimeMetricsRule allow enabling and configuring particular group of runtime/metrics. +// TODO(bwplotka): Consider adding ability to adjust buckets. +type GoRuntimeMetricsRule struct { + // Matcher represents RE2 expression will match the runtime/metrics from https://golang.bg/src/runtime/metrics/description.go + // Use `regexp.MustCompile` or `regexp.Compile` to create this field. + Matcher *regexp.Regexp +} + +// WithGoCollectorRuntimeMetrics allows enabling and configuring particular group of runtime/metrics. +// See the list of metrics https://golang.bg/src/runtime/metrics/description.go (pick the Go version you use there!). +// You can use this option in repeated manner, which will add new rules. The order of rules is important, the last rule +// that matches particular metrics is applied. +func WithGoCollectorRuntimeMetrics(rules ...GoRuntimeMetricsRule) func(options *internal.GoCollectorOptions) { + rs := make([]internal.GoCollectorRule, len(rules)) + for i, r := range rules { + rs[i] = internal.GoCollectorRule{ + Matcher: r.Matcher, + } + } + + return func(o *internal.GoCollectorOptions) { + o.RuntimeMetricRules = append(o.RuntimeMetricRules, rs...) + } +} + +// WithoutGoCollectorRuntimeMetrics allows disabling group of runtime/metrics that you might have added in WithGoCollectorRuntimeMetrics. +// It behaves similarly to WithGoCollectorRuntimeMetrics just with deny-list semantics. +func WithoutGoCollectorRuntimeMetrics(matchers ...*regexp.Regexp) func(options *internal.GoCollectorOptions) { + rs := make([]internal.GoCollectorRule, len(matchers)) + for i, m := range matchers { + rs[i] = internal.GoCollectorRule{ + Matcher: m, + Deny: true, + } + } + + return func(o *internal.GoCollectorOptions) { + o.RuntimeMetricRules = append(o.RuntimeMetricRules, rs...) + } +} + +// GoCollectionOption represents Go collection option flag. +// Deprecated. type GoCollectionOption uint32 const ( - // GoRuntimeMemStatsCollection represents the metrics represented by runtime.MemStats structure such as - // go_memstats_alloc_bytes - // go_memstats_alloc_bytes_total - // go_memstats_sys_bytes - // go_memstats_lookups_total - // go_memstats_mallocs_total - // go_memstats_frees_total - // go_memstats_heap_alloc_bytes - // go_memstats_heap_sys_bytes - // go_memstats_heap_idle_bytes - // go_memstats_heap_inuse_bytes - // go_memstats_heap_released_bytes - // go_memstats_heap_objects - // go_memstats_stack_inuse_bytes - // go_memstats_stack_sys_bytes - // go_memstats_mspan_inuse_bytes - // go_memstats_mspan_sys_bytes - // go_memstats_mcache_inuse_bytes - // go_memstats_mcache_sys_bytes - // go_memstats_buck_hash_sys_bytes - // go_memstats_gc_sys_bytes - // go_memstats_other_sys_bytes - // go_memstats_next_gc_bytes - // so the metrics known from pre client_golang v1.12.0, except skipped go_memstats_gc_cpu_fraction (see - // https://github.com/prometheus/client_golang/issues/842#issuecomment-861812034 for explanation. - // - // NOTE that this mode represents runtime.MemStats statistics, but they are - // actually implemented using new runtime/metrics package. - // Deprecated: Use GoRuntimeMetricsCollection instead going forward. + // GoRuntimeMemStatsCollection represents the metrics represented by runtime.MemStats structure. + // Deprecated. Use WithGoCollectorMemStatsMetricsDisabled() function to disable those metrics in the collector. GoRuntimeMemStatsCollection GoCollectionOption = 1 << iota - // GoRuntimeMetricsCollection is the new set of metrics represented by runtime/metrics package and follows - // consistent naming. The exposed metric set depends on Go version, but it is controlled against - // unexpected cardinality. This set has overlapping information with GoRuntimeMemStatsCollection, just with - // new names. GoRuntimeMetricsCollection is what is recommended for using going forward. + // GoRuntimeMetricsCollection is the new set of metrics represented by runtime/metrics package. + // Deprecated. Use WithGoCollectorRuntimeMetrics(GoRuntimeMetricsRule{Matcher: regexp.MustCompile("/.*")}) + // function to enable those metrics in the collector. GoRuntimeMetricsCollection ) -// WithGoCollections allows enabling different collections for Go collector on top of base metrics -// like go_goroutines, go_threads, go_gc_duration_seconds, go_memstats_last_gc_time_seconds, go_info. -// -// Check GoRuntimeMemStatsCollection and GoRuntimeMetricsCollection for more details. You can use none, -// one or more collections at once. For example: -// WithGoCollections(GoRuntimeMemStatsCollection | GoRuntimeMetricsCollection) means both GoRuntimeMemStatsCollection -// metrics and GoRuntimeMetricsCollection will be exposed. -// -// The current default is GoRuntimeMemStatsCollection, so the compatibility mode with -// client_golang pre v1.12 (move to runtime/metrics). -//nolint:staticcheck // Ignore SA1019 until v2. -func WithGoCollections(flags GoCollectionOption) func(options *prometheus.GoCollectorOptions) { - return func(o *goOptions) { - o.EnabledCollections = uint32(flags) +// WithGoCollections allows enabling different collections for Go collector on top of base metrics. +// Deprecated. Use WithGoCollectorRuntimeMetrics() and WithGoCollectorMemStatsMetricsDisabled() instead to control metrics. +func WithGoCollections(flags GoCollectionOption) func(options *internal.GoCollectorOptions) { + return func(options *internal.GoCollectorOptions) { + if flags&GoRuntimeMemStatsCollection == 0 { + WithGoCollectorMemStatsMetricsDisabled()(options) + } + + if flags&GoRuntimeMetricsCollection != 0 { + WithGoCollectorRuntimeMetrics(GoRuntimeMetricsRule{Matcher: regexp.MustCompile("/.*")})(options) + } } } // NewGoCollector returns a collector that exports metrics about the current Go -// process using debug.GCStats using runtime/metrics. -func NewGoCollector(opts ...goOption) prometheus.Collector { +// process using debug.GCStats (base metrics) and runtime/metrics (both in MemStats style and new ones). +func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) prometheus.Collector { //nolint:staticcheck // Ignore SA1019 until v2. - promPkgOpts := make([]func(o *prometheus.GoCollectorOptions), len(opts)) - for i, opt := range opts { - promPkgOpts[i] = opt - } - //nolint:staticcheck // Ignore SA1019 until v2. - return prometheus.NewGoCollector(promPkgOpts...) + return prometheus.NewGoCollector(opts...) } diff --git a/prometheus/collectors/go_collector_latest_test.go b/prometheus/collectors/go_collector_latest_test.go index 126864c..96cdb71 100644 --- a/prometheus/collectors/go_collector_latest_test.go +++ b/prometheus/collectors/go_collector_latest_test.go @@ -18,15 +18,31 @@ package collectors import ( "encoding/json" + "log" + "net/http" + "reflect" + "regexp" + "sort" "testing" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) +var baseMetrics = []string{ + "go_gc_duration_seconds", + "go_goroutines", + "go_info", + "go_memstats_last_gc_time_seconds", + "go_threads", +} + func TestGoCollectorMarshalling(t *testing.T) { reg := prometheus.NewRegistry() reg.MustRegister(NewGoCollector( - WithGoCollections(GoRuntimeMemStatsCollection | GoRuntimeMetricsCollection), + WithGoCollectorRuntimeMetrics(GoRuntimeMetricsRule{ + Matcher: regexp.MustCompile("/.*"), + }), )) result, err := reg.Gather() if err != nil { @@ -37,3 +53,248 @@ func TestGoCollectorMarshalling(t *testing.T) { t.Errorf("json marshalling shoud not fail, %v", err) } } + +func TestWithGoCollectorMemStatsMetricsDisabled(t *testing.T) { + reg := prometheus.NewRegistry() + reg.MustRegister(NewGoCollector( + WithGoCollectorMemStatsMetricsDisabled(), + )) + result, err := reg.Gather() + if err != nil { + t.Fatal(err) + } + + got := []string{} + for _, r := range result { + got = append(got, r.GetName()) + } + + if !reflect.DeepEqual(got, baseMetrics) { + t.Errorf("got %v, want %v", got, baseMetrics) + } +} + +func TestGoCollectorAllowList(t *testing.T) { + for _, test := range []struct { + name string + rules []GoRuntimeMetricsRule + expected []string + }{ + { + name: "Without any rules", + rules: nil, + expected: baseMetrics, + }, + { + name: "allow all", + rules: []GoRuntimeMetricsRule{MetricsAll}, + expected: withBaseMetrics([]string{ + "go_gc_cycles_automatic_gc_cycles_total", + "go_gc_cycles_forced_gc_cycles_total", + "go_gc_cycles_total_gc_cycles_total", + "go_gc_heap_allocs_by_size_bytes", + "go_gc_heap_allocs_bytes_total", + "go_gc_heap_allocs_objects_total", + "go_gc_heap_frees_by_size_bytes", + "go_gc_heap_frees_bytes_total", + "go_gc_heap_frees_objects_total", + "go_gc_heap_goal_bytes", + "go_gc_heap_objects_objects", + "go_gc_heap_tiny_allocs_objects_total", + "go_gc_pauses_seconds", + "go_memory_classes_heap_free_bytes", + "go_memory_classes_heap_objects_bytes", + "go_memory_classes_heap_released_bytes", + "go_memory_classes_heap_stacks_bytes", + "go_memory_classes_heap_unused_bytes", + "go_memory_classes_metadata_mcache_free_bytes", + "go_memory_classes_metadata_mcache_inuse_bytes", + "go_memory_classes_metadata_mspan_free_bytes", + "go_memory_classes_metadata_mspan_inuse_bytes", + "go_memory_classes_metadata_other_bytes", + "go_memory_classes_os_stacks_bytes", + "go_memory_classes_other_bytes", + "go_memory_classes_profiling_buckets_bytes", + "go_memory_classes_total_bytes", + "go_sched_goroutines_goroutines", + "go_sched_latencies_seconds", + }), + }, + { + name: "allow GC", + rules: []GoRuntimeMetricsRule{MetricsGC}, + expected: withBaseMetrics([]string{ + "go_gc_cycles_automatic_gc_cycles_total", + "go_gc_cycles_forced_gc_cycles_total", + "go_gc_cycles_total_gc_cycles_total", + "go_gc_heap_allocs_by_size_bytes", + "go_gc_heap_allocs_bytes_total", + "go_gc_heap_allocs_objects_total", + "go_gc_heap_frees_by_size_bytes", + "go_gc_heap_frees_bytes_total", + "go_gc_heap_frees_objects_total", + "go_gc_heap_goal_bytes", + "go_gc_heap_objects_objects", + "go_gc_heap_tiny_allocs_objects_total", + "go_gc_pauses_seconds", + }), + }, + { + name: "allow Memory", + rules: []GoRuntimeMetricsRule{MetricsMemory}, + expected: withBaseMetrics([]string{ + "go_memory_classes_heap_free_bytes", + "go_memory_classes_heap_objects_bytes", + "go_memory_classes_heap_released_bytes", + "go_memory_classes_heap_stacks_bytes", + "go_memory_classes_heap_unused_bytes", + "go_memory_classes_metadata_mcache_free_bytes", + "go_memory_classes_metadata_mcache_inuse_bytes", + "go_memory_classes_metadata_mspan_free_bytes", + "go_memory_classes_metadata_mspan_inuse_bytes", + "go_memory_classes_metadata_other_bytes", + "go_memory_classes_os_stacks_bytes", + "go_memory_classes_other_bytes", + "go_memory_classes_profiling_buckets_bytes", + "go_memory_classes_total_bytes", + }), + }, + { + name: "allow Scheduler", + rules: []GoRuntimeMetricsRule{MetricsScheduler}, + expected: []string{ + "go_gc_duration_seconds", + "go_goroutines", + "go_info", + "go_memstats_last_gc_time_seconds", + "go_sched_goroutines_goroutines", + "go_sched_latencies_seconds", + "go_threads", + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + reg := prometheus.NewRegistry() + reg.MustRegister(NewGoCollector( + WithGoCollectorMemStatsMetricsDisabled(), + WithGoCollectorRuntimeMetrics(test.rules...), + )) + result, err := reg.Gather() + if err != nil { + t.Fatal(err) + } + + got := []string{} + for _, r := range result { + got = append(got, r.GetName()) + } + + if !reflect.DeepEqual(got, test.expected) { + t.Errorf("got %v, want %v", got, test.expected) + } + }) + } +} + +func withBaseMetrics(metricNames []string) []string { + metricNames = append(metricNames, baseMetrics...) + sort.Strings(metricNames) + return metricNames +} + +func TestGoCollectorDenyList(t *testing.T) { + for _, test := range []struct { + name string + matchers []*regexp.Regexp + expected []string + }{ + { + name: "Without any matchers", + matchers: nil, + expected: baseMetrics, + }, + { + name: "deny all", + matchers: []*regexp.Regexp{regexp.MustCompile("/.*")}, + expected: baseMetrics, + }, + { + name: "deny gc and scheduler latency", + matchers: []*regexp.Regexp{ + regexp.MustCompile("^/gc/.*"), + regexp.MustCompile("^/sched/latencies:.*"), + }, + expected: baseMetrics, + }, + } { + t.Run(test.name, func(t *testing.T) { + reg := prometheus.NewRegistry() + reg.MustRegister(NewGoCollector( + WithGoCollectorMemStatsMetricsDisabled(), + WithoutGoCollectorRuntimeMetrics(test.matchers...), + )) + result, err := reg.Gather() + if err != nil { + t.Fatal(err) + } + + got := []string{} + for _, r := range result { + got = append(got, r.GetName()) + } + + if !reflect.DeepEqual(got, test.expected) { + t.Errorf("got %v, want %v", got, test.expected) + } + }) + } +} + +func ExampleGoCollector() { + reg := prometheus.NewRegistry() + + // Register the GoCollector with the default options. Only the base metrics will be enabled. + reg.MustRegister(NewGoCollector()) + + http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func ExampleGoCollector_WithAdvancedGoMetrics() { + reg := prometheus.NewRegistry() + + // Enable Go metrics with pre-defined rules. Or your custom rules. + reg.MustRegister( + NewGoCollector( + WithGoCollectorMemStatsMetricsDisabled(), + WithGoCollectorRuntimeMetrics( + MetricsScheduler, + MetricsMemory, + GoRuntimeMetricsRule{ + Matcher: regexp.MustCompile("^/mycustomrule.*"), + }, + ), + WithoutGoCollectorRuntimeMetrics(regexp.MustCompile("^/gc/.*")), + )) + + http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func ExampleGoCollector_DefaultRegister() { + // Unregister the default GoCollector. + prometheus.Unregister(NewGoCollector()) + + // Register the default GoCollector with a custom config. + prometheus.MustRegister(NewGoCollector(WithGoCollectorRuntimeMetrics( + MetricsScheduler, + MetricsGC, + GoRuntimeMetricsRule{ + Matcher: regexp.MustCompile("^/mycustomrule.*"), + }, + ), + )) + + http.Handle("/metrics", promhttp.Handler()) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/prometheus/go_collector.go b/prometheus/go_collector.go index 5fd7300..ad9a71a 100644 --- a/prometheus/go_collector.go +++ b/prometheus/go_collector.go @@ -19,6 +19,10 @@ import ( "time" ) +// goRuntimeMemStats provides the metrics initially provided by runtime.ReadMemStats. +// From Go 1.17 those similar (and better) statistics are provided by runtime/metrics, so +// while eval closure works on runtime.MemStats, the struct from Go 1.17+ is +// populated using runtime/metrics. func goRuntimeMemStats() memStatsMetrics { return memStatsMetrics{ { @@ -224,7 +228,7 @@ func newBaseGoCollector() baseGoCollector { "A summary of the pause duration of garbage collection cycles.", nil, nil), gcLastTimeDesc: NewDesc( - memstatNamespace("last_gc_time_seconds"), + "go_memstats_last_gc_time_seconds", "Number of seconds since 1970 of last garbage collection.", nil, nil), goInfoDesc: NewDesc( @@ -270,7 +274,6 @@ func memstatNamespace(s string) string { // memStatsMetrics provide description, evaluator, runtime/metrics name, and // value type for memstat metrics. -// TODO(bwplotka): Remove with end Go 1.16 EOL and replace with runtime/metrics.Description type memStatsMetrics []struct { desc *Desc eval func(*runtime.MemStats) float64 diff --git a/prometheus/go_collector_latest.go b/prometheus/go_collector_latest.go index 68a7a15..3a2d55e 100644 --- a/prometheus/go_collector_latest.go +++ b/prometheus/go_collector_latest.go @@ -31,9 +31,11 @@ import ( ) const ( + // constants for strings referenced more than once. goGCHeapTinyAllocsObjects = "/gc/heap/tiny/allocs:objects" goGCHeapAllocsObjects = "/gc/heap/allocs:objects" goGCHeapFreesObjects = "/gc/heap/frees:objects" + goGCHeapFreesBytes = "/gc/heap/frees:bytes" goGCHeapAllocsBytes = "/gc/heap/allocs:bytes" goGCHeapObjects = "/gc/heap/objects:objects" goGCHeapGoalBytes = "/gc/heap/goal:bytes" @@ -53,8 +55,8 @@ const ( goMemoryClassesOtherBytes = "/memory/classes/other:bytes" ) -// runtime/metrics names required for runtimeMemStats like logic. -var rmForMemStats = []string{ +// rmNamesForMemStatsMetrics represents runtime/metrics names required to populate goRuntimeMemStats from like logic. +var rmNamesForMemStatsMetrics = []string{ goGCHeapTinyAllocsObjects, goGCHeapAllocsObjects, goGCHeapFreesObjects, @@ -90,74 +92,90 @@ func bestEffortLookupRM(lookup []string) []metrics.Description { } type goCollector struct { - opt GoCollectorOptions base baseGoCollector // mu protects updates to all fields ensuring a consistent // snapshot is always produced by Collect. mu sync.Mutex - // rm... fields all pertain to the runtime/metrics package. - rmSampleBuf []metrics.Sample - rmSampleMap map[string]*metrics.Sample - rmMetrics []collectorMetric + // Contains all samples that has to retrieved from runtime/metrics (not all of them will be exposed). + sampleBuf []metrics.Sample + // sampleMap allows lookup for MemStats metrics and runtime/metrics histograms for exact sums. + sampleMap map[string]*metrics.Sample + + // rmExposedMetrics represents all runtime/metrics package metrics + // that were configured to be exposed. + rmExposedMetrics []collectorMetric + rmExactSumMapForHist map[string]string // With Go 1.17, the runtime/metrics package was introduced. // From that point on, metric names produced by the runtime/metrics // package could be generated from runtime/metrics names. However, // these differ from the old names for the same values. // - // This field exist to export the same values under the old names + // This field exists to export the same values under the old names // as well. - msMetrics memStatsMetrics + msMetrics memStatsMetrics + msMetricsEnabled bool } -const ( - // Those are not exposed due to need to move Go collector to another package in v2. - // See issue https://github.com/prometheus/client_golang/issues/1030. - goRuntimeMemStatsCollection uint32 = 1 << iota - goRuntimeMetricsCollection -) - -// GoCollectorOptions should not be used be directly by anything, except `collectors` package. -// Use it via collectors package instead. See issue -// https://github.com/prometheus/client_golang/issues/1030. -// -// Deprecated: Use collectors.WithGoCollections -type GoCollectorOptions struct { - // EnabledCollection sets what type of collections collector should expose on top of base collection. - // By default it's goMemStatsCollection | goRuntimeMetricsCollection. - EnabledCollections uint32 +type rmMetricDesc struct { + metrics.Description } -func (c GoCollectorOptions) isEnabled(flag uint32) bool { - return c.EnabledCollections&flag != 0 +func matchRuntimeMetricsRules(rules []internal.GoCollectorRule) []rmMetricDesc { + var descs []rmMetricDesc + for _, d := range metrics.All() { + var ( + deny = true + desc rmMetricDesc + ) + + for _, r := range rules { + if !r.Matcher.MatchString(d.Name) { + continue + } + deny = r.Deny + } + if deny { + continue + } + + desc.Description = d + descs = append(descs, desc) + } + return descs } -const defaultGoCollections = goRuntimeMemStatsCollection +func defaultGoCollectorOptions() internal.GoCollectorOptions { + return internal.GoCollectorOptions{ + RuntimeMetricSumForHist: map[string]string{ + "/gc/heap/allocs-by-size:bytes": goGCHeapAllocsBytes, + "/gc/heap/frees-by-size:bytes": goGCHeapFreesBytes, + }, + RuntimeMetricRules: []internal.GoCollectorRule{ + //{Matcher: regexp.MustCompile("")}, + }, + } +} // NewGoCollector is the obsolete version of collectors.NewGoCollector. // See there for documentation. // // Deprecated: Use collectors.NewGoCollector instead. -func NewGoCollector(opts ...func(o *GoCollectorOptions)) Collector { - opt := GoCollectorOptions{EnabledCollections: defaultGoCollections} +func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector { + opt := defaultGoCollectorOptions() for _, o := range opts { o(&opt) } - var descriptions []metrics.Description - if opt.isEnabled(goRuntimeMetricsCollection) { - descriptions = metrics.All() - } else if opt.isEnabled(goRuntimeMemStatsCollection) { - descriptions = bestEffortLookupRM(rmForMemStats) - } + exposedDescriptions := matchRuntimeMetricsRules(opt.RuntimeMetricRules) // Collect all histogram samples so that we can get their buckets. // The API guarantees that the buckets are always fixed for the lifetime // of the process. var histograms []metrics.Sample - for _, d := range descriptions { + for _, d := range exposedDescriptions { if d.Kind == metrics.KindFloat64Histogram { histograms = append(histograms, metrics.Sample{Name: d.Name}) } @@ -172,13 +190,14 @@ func NewGoCollector(opts ...func(o *GoCollectorOptions)) Collector { bucketsMap[histograms[i].Name] = histograms[i].Value.Float64Histogram().Buckets } - // Generate a Desc and ValueType for each runtime/metrics metric. - metricSet := make([]collectorMetric, 0, len(descriptions)) - sampleBuf := make([]metrics.Sample, 0, len(descriptions)) - sampleMap := make(map[string]*metrics.Sample, len(descriptions)) - for i := range descriptions { - d := &descriptions[i] - namespace, subsystem, name, ok := internal.RuntimeMetricsToProm(d) + // Generate a collector for each exposed runtime/metrics metric. + metricSet := make([]collectorMetric, 0, len(exposedDescriptions)) + // SampleBuf is used for reading from runtime/metrics. + // We are assuming the largest case to have stable pointers for sampleMap purposes. + sampleBuf := make([]metrics.Sample, 0, len(exposedDescriptions)+len(opt.RuntimeMetricSumForHist)+len(rmNamesForMemStatsMetrics)) + sampleMap := make(map[string]*metrics.Sample, len(exposedDescriptions)) + for _, d := range exposedDescriptions { + namespace, subsystem, name, ok := internal.RuntimeMetricsToProm(&d.Description) if !ok { // Just ignore this metric; we can't do anything with it here. // If a user decides to use the latest version of Go, we don't want @@ -186,19 +205,17 @@ func NewGoCollector(opts ...func(o *GoCollectorOptions)) Collector { continue } - // Set up sample buffer for reading, and a map - // for quick lookup of sample values. sampleBuf = append(sampleBuf, metrics.Sample{Name: d.Name}) sampleMap[d.Name] = &sampleBuf[len(sampleBuf)-1] var m collectorMetric if d.Kind == metrics.KindFloat64Histogram { - _, hasSum := rmExactSumMap[d.Name] + _, hasSum := opt.RuntimeMetricSumForHist[d.Name] unit := d.Name[strings.IndexRune(d.Name, ':')+1:] m = newBatchHistogram( NewDesc( BuildFQName(namespace, subsystem, name), - d.Description, + d.Description.Description, nil, nil, ), @@ -210,30 +227,61 @@ func NewGoCollector(opts ...func(o *GoCollectorOptions)) Collector { Namespace: namespace, Subsystem: subsystem, Name: name, - Help: d.Description, - }) + Help: d.Description.Description, + }, + ) } else { m = NewGauge(GaugeOpts{ Namespace: namespace, Subsystem: subsystem, Name: name, - Help: d.Description, + Help: d.Description.Description, }) } metricSet = append(metricSet, m) } - var msMetrics memStatsMetrics - if opt.isEnabled(goRuntimeMemStatsCollection) { - msMetrics = goRuntimeMemStats() + // Add exact sum metrics to sampleBuf if not added before. + for _, h := range histograms { + sumMetric, ok := opt.RuntimeMetricSumForHist[h.Name] + if !ok { + continue + } + + if _, ok := sampleMap[sumMetric]; ok { + continue + } + sampleBuf = append(sampleBuf, metrics.Sample{Name: sumMetric}) + sampleMap[sumMetric] = &sampleBuf[len(sampleBuf)-1] } + + var ( + msMetrics memStatsMetrics + msDescriptions []metrics.Description + ) + + if !opt.DisableMemStatsLikeMetrics { + msMetrics = goRuntimeMemStats() + msDescriptions = bestEffortLookupRM(rmNamesForMemStatsMetrics) + + // Check if metric was not exposed before and if not, add to sampleBuf. + for _, mdDesc := range msDescriptions { + if _, ok := sampleMap[mdDesc.Name]; ok { + continue + } + sampleBuf = append(sampleBuf, metrics.Sample{Name: mdDesc.Name}) + sampleMap[mdDesc.Name] = &sampleBuf[len(sampleBuf)-1] + } + } + return &goCollector{ - opt: opt, - base: newBaseGoCollector(), - rmSampleBuf: sampleBuf, - rmSampleMap: sampleMap, - rmMetrics: metricSet, - msMetrics: msMetrics, + base: newBaseGoCollector(), + sampleBuf: sampleBuf, + sampleMap: sampleMap, + rmExposedMetrics: metricSet, + rmExactSumMapForHist: opt.RuntimeMetricSumForHist, + msMetrics: msMetrics, + msMetricsEnabled: !opt.DisableMemStatsLikeMetrics, } } @@ -243,7 +291,7 @@ func (c *goCollector) Describe(ch chan<- *Desc) { for _, i := range c.msMetrics { ch <- i.desc } - for _, m := range c.rmMetrics { + for _, m := range c.rmExposedMetrics { ch <- m.Desc() } } @@ -253,8 +301,12 @@ func (c *goCollector) Collect(ch chan<- Metric) { // Collect base non-memory metrics. c.base.Collect(ch) + if len(c.sampleBuf) == 0 { + return + } + // Collect must be thread-safe, so prevent concurrent use of - // rmSampleBuf. Just read into rmSampleBuf but write all the data + // sampleBuf elements. Just read into sampleBuf but write all the data // we get into our Metrics or MemStats. // // This lock also ensures that the Metrics we send out are all from @@ -268,44 +320,43 @@ func (c *goCollector) Collect(ch chan<- Metric) { c.mu.Lock() defer c.mu.Unlock() - if len(c.rmSampleBuf) > 0 { - // Populate runtime/metrics sample buffer. - metrics.Read(c.rmSampleBuf) - } + // Populate runtime/metrics sample buffer. + metrics.Read(c.sampleBuf) - if c.opt.isEnabled(goRuntimeMetricsCollection) { - // Collect all our metrics from rmSampleBuf. - for i, sample := range c.rmSampleBuf { - // N.B. switch on concrete type because it's significantly more efficient - // than checking for the Counter and Gauge interface implementations. In - // this case, we control all the types here. - switch m := c.rmMetrics[i].(type) { - case *counter: - // Guard against decreases. This should never happen, but a failure - // to do so will result in a panic, which is a harsh consequence for - // a metrics collection bug. - v0, v1 := m.get(), unwrapScalarRMValue(sample.Value) - if v1 > v0 { - m.Add(unwrapScalarRMValue(sample.Value) - m.get()) - } - m.Collect(ch) - case *gauge: - m.Set(unwrapScalarRMValue(sample.Value)) - m.Collect(ch) - case *batchHistogram: - m.update(sample.Value.Float64Histogram(), c.exactSumFor(sample.Name)) - m.Collect(ch) - default: - panic("unexpected metric type") + // Collect all our runtime/metrics user chose to expose from sampleBuf (if any). + for i, metric := range c.rmExposedMetrics { + // We created samples for exposed metrics first in order, so indexes match. + sample := c.sampleBuf[i] + + // N.B. switch on concrete type because it's significantly more efficient + // than checking for the Counter and Gauge interface implementations. In + // this case, we control all the types here. + switch m := metric.(type) { + case *counter: + // Guard against decreases. This should never happen, but a failure + // to do so will result in a panic, which is a harsh consequence for + // a metrics collection bug. + v0, v1 := m.get(), unwrapScalarRMValue(sample.Value) + if v1 > v0 { + m.Add(unwrapScalarRMValue(sample.Value) - m.get()) } + m.Collect(ch) + case *gauge: + m.Set(unwrapScalarRMValue(sample.Value)) + m.Collect(ch) + case *batchHistogram: + m.update(sample.Value.Float64Histogram(), c.exactSumFor(sample.Name)) + m.Collect(ch) + default: + panic("unexpected metric type") } } - // ms is a dummy MemStats that we populate ourselves so that we can - // populate the old metrics from it if goMemStatsCollection is enabled. - if c.opt.isEnabled(goRuntimeMemStatsCollection) { + if c.msMetricsEnabled { + // ms is a dummy MemStats that we populate ourselves so that we can + // populate the old metrics from it if goMemStatsCollection is enabled. var ms runtime.MemStats - memStatsFromRM(&ms, c.rmSampleMap) + memStatsFromRM(&ms, c.sampleMap) for _, i := range c.msMetrics { ch <- MustNewConstMetric(i.desc, i.valType, i.eval(&ms)) } @@ -336,11 +387,6 @@ func unwrapScalarRMValue(v metrics.Value) float64 { } } -var rmExactSumMap = map[string]string{ - "/gc/heap/allocs-by-size:bytes": "/gc/heap/allocs:bytes", - "/gc/heap/frees-by-size:bytes": "/gc/heap/frees:bytes", -} - // exactSumFor takes a runtime/metrics metric name (that is assumed to // be of kind KindFloat64Histogram) and returns its exact sum and whether // its exact sum exists. @@ -348,11 +394,11 @@ var rmExactSumMap = map[string]string{ // The runtime/metrics API for histograms doesn't currently expose exact // sums, but some of the other metrics are in fact exact sums of histograms. func (c *goCollector) exactSumFor(rmName string) float64 { - sumName, ok := rmExactSumMap[rmName] + sumName, ok := c.rmExactSumMapForHist[rmName] if !ok { return 0 } - s, ok := c.rmSampleMap[sumName] + s, ok := c.sampleMap[sumName] if !ok { return 0 } diff --git a/prometheus/go_collector_latest_test.go b/prometheus/go_collector_latest_test.go index df18d5d..d64120c 100644 --- a/prometheus/go_collector_latest_test.go +++ b/prometheus/go_collector_latest_test.go @@ -19,6 +19,7 @@ package prometheus import ( "math" "reflect" + "regexp" "runtime" "runtime/metrics" "sync" @@ -30,9 +31,18 @@ import ( ) func TestRmForMemStats(t *testing.T) { - if got, want := len(bestEffortLookupRM(rmForMemStats)), len(rmForMemStats); got != want { + descs := bestEffortLookupRM(rmNamesForMemStatsMetrics) + + if got, want := len(descs), len(rmNamesForMemStatsMetrics); got != want { t.Errorf("got %d, want %d metrics", got, want) } + + for _, d := range descs { + // We don't expect histograms there. + if d.Kind == metrics.KindFloat64Histogram { + t.Errorf("we don't expect to use histograms for MemStats metrics, got %v", d.Name) + } + } } func expectedBaseMetrics() map[string]struct{} { @@ -64,30 +74,43 @@ func addExpectedRuntimeMetrics(metrics map[string]struct{}) map[string]struct{} return metrics } -func TestGoCollector(t *testing.T) { +func TestGoCollector_ExposedMetrics(t *testing.T) { for _, tcase := range []struct { - collections uint32 + opts internal.GoCollectorOptions expectedFQNameSet map[string]struct{} }{ { - collections: 0, + opts: internal.GoCollectorOptions{ + DisableMemStatsLikeMetrics: true, + }, expectedFQNameSet: expectedBaseMetrics(), }, { - collections: goRuntimeMemStatsCollection, + // Default, only MemStats. expectedFQNameSet: addExpectedRuntimeMemStats(expectedBaseMetrics()), }, { - collections: goRuntimeMetricsCollection, + // Get all runtime/metrics without MemStats. + opts: internal.GoCollectorOptions{ + DisableMemStatsLikeMetrics: true, + RuntimeMetricRules: []internal.GoCollectorRule{ + {Matcher: regexp.MustCompile("/.*")}, + }, + }, expectedFQNameSet: addExpectedRuntimeMetrics(expectedBaseMetrics()), }, { - collections: goRuntimeMemStatsCollection | goRuntimeMetricsCollection, + // Get all runtime/metrics and MemStats. + opts: internal.GoCollectorOptions{ + RuntimeMetricRules: []internal.GoCollectorRule{ + {Matcher: regexp.MustCompile("/.*")}, + }, + }, expectedFQNameSet: addExpectedRuntimeMemStats(addExpectedRuntimeMetrics(expectedBaseMetrics())), }, } { if ok := t.Run("", func(t *testing.T) { - goMetrics := collectGoMetrics(t, tcase.collections) + goMetrics := collectGoMetrics(t, tcase.opts) goMetricSet := make(map[string]Metric) for _, m := range goMetrics { goMetricSet[m.Desc().fqName] = m @@ -118,7 +141,11 @@ func TestGoCollector(t *testing.T) { var sink interface{} func TestBatchHistogram(t *testing.T) { - goMetrics := collectGoMetrics(t, goRuntimeMetricsCollection) + goMetrics := collectGoMetrics(t, internal.GoCollectorOptions{ + RuntimeMetricRules: []internal.GoCollectorRule{ + {Matcher: regexp.MustCompile("/.*")}, + }, + }) var mhist Metric for _, m := range goMetrics { @@ -145,7 +172,8 @@ func TestBatchHistogram(t *testing.T) { for i := 0; i < 100; i++ { sink = make([]byte, 128) } - collectGoMetrics(t, defaultGoCollections) + + collectGoMetrics(t, defaultGoCollectorOptions()) for i, v := range hist.counts { if v != countsCopy[i] { t.Error("counts changed during new collection") @@ -194,11 +222,13 @@ func TestBatchHistogram(t *testing.T) { } } -func collectGoMetrics(t *testing.T, enabledCollections uint32) []Metric { +func collectGoMetrics(t *testing.T, opts internal.GoCollectorOptions) []Metric { t.Helper() - c := NewGoCollector(func(o *GoCollectorOptions) { - o.EnabledCollections = enabledCollections + c := NewGoCollector(func(o *internal.GoCollectorOptions) { + o.DisableMemStatsLikeMetrics = opts.DisableMemStatsLikeMetrics + o.RuntimeMetricSumForHist = opts.RuntimeMetricSumForHist + o.RuntimeMetricRules = opts.RuntimeMetricRules }).(*goCollector) // Collect all metrics. @@ -222,7 +252,7 @@ func collectGoMetrics(t *testing.T, enabledCollections uint32) []Metric { func TestMemStatsEquivalence(t *testing.T) { var msReal, msFake runtime.MemStats - descs := bestEffortLookupRM(rmForMemStats) + descs := bestEffortLookupRM(rmNamesForMemStatsMetrics) samples := make([]metrics.Sample, len(descs)) samplesMap := make(map[string]*metrics.Sample) @@ -269,7 +299,12 @@ func TestMemStatsEquivalence(t *testing.T) { } func TestExpectedRuntimeMetrics(t *testing.T) { - goMetrics := collectGoMetrics(t, goRuntimeMemStatsCollection|goRuntimeMetricsCollection) + goMetrics := collectGoMetrics(t, internal.GoCollectorOptions{ + DisableMemStatsLikeMetrics: true, + RuntimeMetricRules: []internal.GoCollectorRule{ + {Matcher: regexp.MustCompile("/.*")}, + }, + }) goMetricSet := make(map[string]Metric) for _, m := range goMetrics { goMetricSet[m.Desc().fqName] = m diff --git a/prometheus/internal/go_collector_options.go b/prometheus/internal/go_collector_options.go new file mode 100644 index 0000000..723b45d --- /dev/null +++ b/prometheus/internal/go_collector_options.go @@ -0,0 +1,32 @@ +// Copyright 2021 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 internal + +import "regexp" + +type GoCollectorRule struct { + Matcher *regexp.Regexp + Deny bool +} + +// GoCollectorOptions should not be used be directly by anything, except `collectors` package. +// Use it via collectors package instead. See issue +// https://github.com/prometheus/client_golang/issues/1030. +// +// This is internal, so external users only can use it via `collector.WithGoCollector*` methods +type GoCollectorOptions struct { + DisableMemStatsLikeMetrics bool + RuntimeMetricSumForHist map[string]string + RuntimeMetricRules []GoCollectorRule +} From 64435fc00ac419bb878a3f9c9658e8353c19a7cd Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Fri, 5 Aug 2022 20:31:42 +0200 Subject: [PATCH 16/20] Cut 1.13.0 (#1110) Signed-off-by: bwplotka --- CHANGELOG.md | 20 ++++++++++++++++++-- VERSION | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 504f0fb..faccd3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,26 @@ ## Unreleased -* [CHANGE] Minimum required Go version is now 1.16. +## 1.13.0 / 2022-08-05 + +* [CHANGE] Minimum required Go version is now 1.17 (we also test client_golang against new 1.19 version). +* [ENHANCEMENT] Added `prometheus.TransactionalGatherer` interface for `promhttp.Handler` use which allows using low allocation update techniques for custom collectors. #989 +* [ENHANCEMENT] Added exemplar support to `prometheus.NewConstHistogram`. See [`ExampleNewConstHistogram_WithExemplar`](prometheus/examples_test.go#L602) example on how to use it. #986 +* [ENHANCEMENT] `prometheus/push.Pusher` has now context aware methods that pass context to HTTP request. #1028 +* [ENHANCEMENT] `prometheus/push.Pusher` has now `Error` method that retrieve last error. #1075 +* [ENHANCEMENT] `testutil.GatherAndCompare` provides now readable diff on failed comparisons. #998 +* [ENHANCEMENT] Query API now supports timeouts. #1014 +* [ENHANCEMENT] New `MetricVec` method `DeletePartialMatch(labels Labels)` for deleting all metrics that match provided labels. #1013 +* [ENHANCEMENT] `api.Config` now accepts passing custom `*http.Client`. #1025 +* [BUGFIX] Raise exemplar labels limit from 64 to 128 bytes as specified in OpenMetrics spec. #1091 +* [BUGFIX] Allow adding exemplar to +Inf bucket to const histograms. #1094 +* [ENHANCEMENT] Most `promhttp.Instrument*` middlewares now supports adding exemplars to metrics. This allows hooking those to your tracing middleware that retrieves trace ID and put it in exemplar if present. #1055 +* [ENHANCEMENT] Added `testutil.ScrapeAndCompare` method. #1043 +* [BUGFIX] Fixed `GopherJS` build support. #897 +* [ENHANCEMENT] :warning: Added way to specify what `runtime/metrics` `collectors.NewGoCollector` should use. See [`ExampleGoCollector_WithAdvancedGoMetrics`](prometheus/collectors/go_collector_latest_test.go#L263). #1102 ## 1.12.2 / 2022-05-13 -* [CHANGE] Added `collectors.WithGoCollections` that allows to choose what collection of Go runtime metrics user wants: Equivalent of [`MemStats` structure](https://pkg.go.dev/runtime#MemStats) configured using `GoRuntimeMemStatsCollection`, new based on dedicated [runtime/metrics](https://pkg.go.dev/runtime/metrics) metrics represented by `GoRuntimeMetricsCollection` option, or both by specifying `GoRuntimeMemStatsCollection | GoRuntimeMetricsCollection` flag. +* [CHANGE] Added `collectors.WithGoCollections` that allows to choose what collection of Go runtime metrics user wants: Equivalent of [`MemStats` structure](https://pkg.go.dev/runtime#MemStats) configured using `GoRuntimeMemStatsCollection`, new based on dedicated [runtime/metrics](https://pkg.go.dev/runtime/metrics) metrics represented by `GoRuntimeMetricsCollection` option, or both by specifying `GoRuntimeMemStatsCollection | GoRuntimeMetricsCollection` flag. #1031 * [CHANGE] :warning: Change in `collectors.NewGoCollector` metrics: Reverting addition of new ~80 runtime metrics by default. You can enable this back with `GoRuntimeMetricsCollection` option or `GoRuntimeMemStatsCollection | GoRuntimeMetricsCollection` for smooth transition. * [BUGFIX] Fixed the bug that causes generated histogram metric names to end with `_total`. ⚠️ This changes 3 metric names in the new Go collector that was reverted from default in this release. * `go_gc_heap_allocs_by_size_bytes_total` -> `go_gc_heap_allocs_by_size_bytes`, diff --git a/VERSION b/VERSION index 6b89d58..feaae22 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.2 +1.13.0 From 1e61b8ea3c3c73b0fa3dd9478a48ddf2d822e97c Mon Sep 17 00:00:00 2001 From: PrometheusBot Date: Fri, 5 Aug 2022 22:38:30 +0200 Subject: [PATCH 17/20] Update common Prometheus files (#1111) Signed-off-by: prombot --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 8f069d3..6034bcb 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -20,7 +20,7 @@ jobs: - name: install Go uses: actions/setup-go@v2 with: - go-version-file: .go-version + go-version: 1.18.x - name: Install snmp_exporter/generator dependencies run: sudo apt-get update && sudo apt-get -y install libsnmp-dev if: github.repository == 'prometheus/snmp_exporter' From f73e3cc0e2f1cba6e9784c8eeddc39b6f310a5f8 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Mon, 22 Aug 2022 01:28:46 -0400 Subject: [PATCH 18/20] Fix double-counting bug in promhttp.InstrumentRoundTripperCounter (#1118) Signed-off-by: Dave Henderson Signed-off-by: Dave Henderson --- prometheus/promhttp/instrument_client.go | 1 - prometheus/promhttp/instrument_client_test.go | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/prometheus/promhttp/instrument_client.go b/prometheus/promhttp/instrument_client.go index 097aff2..57bb5f9 100644 --- a/prometheus/promhttp/instrument_client.go +++ b/prometheus/promhttp/instrument_client.go @@ -78,7 +78,6 @@ func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.Rou 1, rtOpts.getExemplarFn(r.Context()), ) - counter.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)).Inc() } return resp, err } diff --git a/prometheus/promhttp/instrument_client_test.go b/prometheus/promhttp/instrument_client_test.go index 98667e8..ce7c4da 100644 --- a/prometheus/promhttp/instrument_client_test.go +++ b/prometheus/promhttp/instrument_client_test.go @@ -25,6 +25,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" @@ -250,6 +251,19 @@ func TestClientMiddlewareAPI_WithRequestContext(t *testing.T) { t.Errorf("metric family %s must not be empty", mf.GetName()) } } + + // make sure counters aren't double-incremented (see #1117) + expected := ` + # HELP client_api_requests_total A counter for requests from the wrapped client. + # TYPE client_api_requests_total counter + client_api_requests_total{code="200",method="get"} 1 + ` + + if err := testutil.GatherAndCompare(reg, strings.NewReader(expected), + "client_api_requests_total", + ); err != nil { + t.Fatal(err) + } } func TestClientMiddlewareAPIWithRequestContextTimeout(t *testing.T) { From 4c41dfbcd5a413804fc29c538d0db225fb2be664 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Mon, 22 Aug 2022 11:31:08 -0400 Subject: [PATCH 19/20] Clarify exemplar(Add|Observe) by renaming to (add|observe)WithExemplar (#1122) * Clarify exemplarAdd by renaming to addWithExemplar Signed-off-by: Dave Henderson * Documenting addWithExemplar Signed-off-by: Dave Henderson * Also rename exemplarObserve to follow the same pattern Signed-off-by: Dave Henderson Signed-off-by: Dave Henderson --- prometheus/promhttp/instrument_client.go | 4 ++-- prometheus/promhttp/instrument_server.go | 24 ++++++++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/prometheus/promhttp/instrument_client.go b/prometheus/promhttp/instrument_client.go index 57bb5f9..2108678 100644 --- a/prometheus/promhttp/instrument_client.go +++ b/prometheus/promhttp/instrument_client.go @@ -73,7 +73,7 @@ func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.Rou return func(r *http.Request) (*http.Response, error) { resp, err := next.RoundTrip(r) if err == nil { - exemplarAdd( + addWithExemplar( counter.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)), 1, rtOpts.getExemplarFn(r.Context()), @@ -116,7 +116,7 @@ func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundT start := time.Now() resp, err := next.RoundTrip(r) if err == nil { - exemplarObserve( + observeWithExemplar( obs.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)), time.Since(start).Seconds(), rtOpts.getExemplarFn(r.Context()), diff --git a/prometheus/promhttp/instrument_server.go b/prometheus/promhttp/instrument_server.go index bfe5009..cca67a7 100644 --- a/prometheus/promhttp/instrument_server.go +++ b/prometheus/promhttp/instrument_server.go @@ -28,7 +28,9 @@ import ( // magicString is used for the hacky label test in checkLabels. Remove once fixed. const magicString = "zZgWfBxLqvG8kc8IMv3POi2Bb0tZI3vAnBx+gBaFi9FyPzB/CzKUer1yufDa" -func exemplarObserve(obs prometheus.Observer, val float64, labels map[string]string) { +// observeWithExemplar is a wrapper for [prometheus.ExemplarAdder.ExemplarObserver], +// which falls back to [prometheus.Observer.Observe] if no labels are provided. +func observeWithExemplar(obs prometheus.Observer, val float64, labels map[string]string) { if labels == nil { obs.Observe(val) return @@ -36,7 +38,9 @@ func exemplarObserve(obs prometheus.Observer, val float64, labels map[string]str obs.(prometheus.ExemplarObserver).ObserveWithExemplar(val, labels) } -func exemplarAdd(obs prometheus.Counter, val float64, labels map[string]string) { +// addWithExemplar is a wrapper for [prometheus.ExemplarAdder.AddWithExemplar], +// which falls back to [prometheus.Counter.Add] if no labels are provided. +func addWithExemplar(obs prometheus.Counter, val float64, labels map[string]string) { if labels == nil { obs.Add(val) return @@ -91,7 +95,7 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op d := newDelegator(w, nil) next.ServeHTTP(d, r) - exemplarObserve( + observeWithExemplar( obs.With(labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()), @@ -103,7 +107,7 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op now := time.Now() next.ServeHTTP(w, r) - exemplarObserve( + observeWithExemplar( obs.With(labels(code, method, r.Method, 0, hOpts.extraMethods...)), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()), @@ -141,7 +145,7 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler, d := newDelegator(w, nil) next.ServeHTTP(d, r) - exemplarAdd( + addWithExemplar( counter.With(labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)), 1, hOpts.getExemplarFn(r.Context()), @@ -151,7 +155,7 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler, return func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) - exemplarAdd( + addWithExemplar( counter.With(labels(code, method, r.Method, 0, hOpts.extraMethods...)), 1, hOpts.getExemplarFn(r.Context()), @@ -192,7 +196,7 @@ func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Ha return func(w http.ResponseWriter, r *http.Request) { now := time.Now() d := newDelegator(w, func(status int) { - exemplarObserve( + observeWithExemplar( obs.With(labels(code, method, r.Method, status, hOpts.extraMethods...)), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()), @@ -233,7 +237,7 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler, d := newDelegator(w, nil) next.ServeHTTP(d, r) size := computeApproximateRequestSize(r) - exemplarObserve( + observeWithExemplar( obs.With(labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)), float64(size), hOpts.getExemplarFn(r.Context()), @@ -244,7 +248,7 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler, return func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) size := computeApproximateRequestSize(r) - exemplarObserve( + observeWithExemplar( obs.With(labels(code, method, r.Method, 0, hOpts.extraMethods...)), float64(size), hOpts.getExemplarFn(r.Context()), @@ -282,7 +286,7 @@ func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w, nil) next.ServeHTTP(d, r) - exemplarObserve( + observeWithExemplar( obs.With(labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)), float64(d.Written()), hOpts.getExemplarFn(r.Context()), From 83d56b1144a0c2eb10d399e7abbae3333bebc463 Mon Sep 17 00:00:00 2001 From: Robert Fratto Date: Tue, 23 Aug 2022 05:09:29 -0400 Subject: [PATCH 20/20] Extend prometheus.Registry to implement Collector (#1103) * prometheus: implement Collector interface for Registry This change allows Registries to be used as Collectors. This enables new instances of Registry to be passed to ephemeral subroutines for collecting metrics from subroutines which are still running: ```go package main import ( "fmt" "github.com/prometheus/client_golang/prometheus" ) func main() { globalReg := prometheus.NewRegistry() for i := 0; i < 100; i++ { workerReg := prometheus.WrapRegistererWith(prometheus.Labels{ // Add an ID label so registered metrics from workers don't // collide. "worker_id": fmt.Sprintf("%d", i), }, prometheus.NewRegistry() globalReg.MustRegister(workerReg) go func(i int) { runWorker(workerReg) // Unregister any metrics the worker may have created. globalReg.Unregister(workerReg) }(i) } } // runWorker runs a worker, registering worker-specific metrics. func runWorker(reg *prometheus.Registry) { // ... register metrics ... // ... do work ... } ``` This change makes it easier to avoid leaking metrics from subroutines which do not consistently properly unregister metrics. Signed-off-by: Robert Fratto * fix grammar in doc comment Signed-off-by: Robert Fratto * document why Registry implements Collector with example Signed-off-by: Robert Fratto Signed-off-by: Robert Fratto --- prometheus/registry.go | 34 +++++++++++++++++++++++++++++++--- prometheus/registry_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/prometheus/registry.go b/prometheus/registry.go index 325f665..09e34d3 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -252,9 +252,12 @@ func (errs MultiError) MaybeUnwrap() error { } // Registry registers Prometheus collectors, collects their metrics, and gathers -// them into MetricFamilies for exposition. It implements both Registerer and -// Gatherer. The zero value is not usable. Create instances with NewRegistry or -// NewPedanticRegistry. +// them into MetricFamilies for exposition. It implements Registerer, Gatherer, +// and Collector. The zero value is not usable. Create instances with +// NewRegistry or NewPedanticRegistry. +// +// Registry implements Collector to allow it to be used for creating groups of +// metrics. See the Grouping example for how this can be done. type Registry struct { mtx sync.RWMutex collectorsByID map[uint64]Collector // ID is a hash of the descIDs. @@ -556,6 +559,31 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() } +// Describe implements Collector. +func (r *Registry) Describe(ch chan<- *Desc) { + r.mtx.RLock() + defer r.mtx.RUnlock() + + // Only report the checked Collectors; unchecked collectors don't report any + // Desc. + for _, c := range r.collectorsByID { + c.Describe(ch) + } +} + +// Collect implements Collector. +func (r *Registry) Collect(ch chan<- Metric) { + r.mtx.RLock() + defer r.mtx.RUnlock() + + for _, c := range r.collectorsByID { + c.Collect(ch) + } + for _, c := range r.uncheckedCollectors { + c.Collect(ch) + } +} + // WriteToTextfile calls Gather on the provided Gatherer, encodes the result in the // Prometheus text format, and writes it to a temporary file. Upon success, the // temporary file is renamed to the provided filename. diff --git a/prometheus/registry_test.go b/prometheus/registry_test.go index f0871ba..3faf67a 100644 --- a/prometheus/registry_test.go +++ b/prometheus/registry_test.go @@ -1254,3 +1254,37 @@ func TestNewMultiTRegistry(t *testing.T) { } }) } + +// This example shows how to use multiple registries for registering and +// unregistering groups of metrics. +func ExampleRegistry_grouping() { + // Create a global registry. + globalReg := prometheus.NewRegistry() + + // Spawn 10 workers, each of which will have their own group of metrics. + for i := 0; i < 10; i++ { + // Create a new registry for each worker, which acts as a group of + // worker-specific metrics. + workerReg := prometheus.NewRegistry() + globalReg.Register(workerReg) + + go func(workerID int) { + // Once the worker is done, it can unregister itself. + defer globalReg.Unregister(workerReg) + + workTime := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "worker_total_work_time_milliseconds", + ConstLabels: prometheus.Labels{ + // Generate a label unique to this worker so its metric doesn't + // collide with the metrics from other workers. + "worker_id": fmt.Sprintf("%d", workerID), + }, + }) + workerReg.MustRegister(workTime) + + start := time.Now() + time.Sleep(time.Millisecond * time.Duration(rand.Intn(100))) + workTime.Add(float64(time.Since(start).Milliseconds())) + }(i) + } +}