From b0993427672443ab4103edceab663e7ec32d3d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?mario=2Evi=C3=B1as?= Date: Mon, 13 May 2019 18:34:52 +0200 Subject: [PATCH 1/5] Included the implementation builder for summaries --- default.go | 1 + go.mod | 2 +- metrics_test.go | 57 ++++++++++++++++++++++++++++++++--- prometheusvanilla/builders.go | 39 ++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/default.go b/default.go index 5701e8e..3d795c3 100644 --- a/default.go +++ b/default.go @@ -14,6 +14,7 @@ func init() { DefaultInitializer.MustAddBuilder(prometheusvanilla.ObserverType, prometheusvanilla.BuildObserver) DefaultInitializer.MustAddBuilder(prometheusvanilla.CounterType, prometheusvanilla.BuildCounter) DefaultInitializer.MustAddBuilder(prometheusvanilla.GaugeType, prometheusvanilla.BuildGauge) + DefaultInitializer.MustAddBuilder(prometheusvanilla.SummaryType, prometheusvanilla.BuildSummary) } // MustAddBuilder will AddBuilder and panic if an error occurs diff --git a/go.mod b/go.mod index 7e67b1c..70ed54d 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v0.9.1 - github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 + github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 // indirect github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d // indirect github.com/stretchr/testify v1.2.2 diff --git a/metrics_test.go b/metrics_test.go index 904c90d..ac4ff02 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -1,10 +1,13 @@ package gotoprom_test import ( + "math" + "strings" "testing" "time" "github.com/cabify/gotoprom" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" @@ -18,10 +21,11 @@ func Test_InitHappyCase(t *testing.T) { } var metrics struct { - HTTPRequestTime func(labels) prometheus.Observer `name:"http_request_count" help:"Time taken to serve a HTTP request" metricsbuckets:"0.001,0.005,0.01,0.05,0.1,0.5,1,5,10"` - DuvelsEmptied func(labels) prometheus.Counter `name:"duvels_emptied" help:"Delirium floor sweep count"` - RubberDuckInTherapy func(labels) prometheus.Gauge `name:"rubber_ducks_in_therapy" help:"Number of rubber ducks who need help after some intense coding"` - NoLabels func() prometheus.Counter `name:"no_labels" help:"Metric without labels"` + HTTPRequestTime func(labels) prometheus.Observer `name:"http_request_count" help:"Time taken to serve a HTTP request" metricsbuckets:"0.001,0.005,0.01,0.05,0.1,0.5,1,5,10"` + DuvelsEmptied func(labels) prometheus.Counter `name:"duvels_emptied" help:"Delirium floor sweep count"` + RubberDuckInTherapy func(labels) prometheus.Gauge `name:"rubber_ducks_in_therapy" help:"Number of rubber ducks who need help after some intense coding"` + BrokenDeploysAccomplished func(labels) prometheus.Summary `name:"broken_deploys_accomplished" help:"Number of deploys that broke production"` + NoLabels func() prometheus.Counter `name:"no_labels" help:"Metric without labels"` } gotoprom.MustInit(&metrics, "delirium") @@ -36,6 +40,7 @@ func Test_InitHappyCase(t *testing.T) { metrics.HTTPRequestTime(theseLabels).Observe(time.Second.Seconds()) metrics.DuvelsEmptied(theseLabels).Add(4) metrics.RubberDuckInTherapy(theseLabels).Set(12) + metrics.BrokenDeploysAccomplished(theseLabels).Observe(20) metrics.NoLabels().Add(288.88) } @@ -275,6 +280,50 @@ func Test_WrongLabels(t *testing.T) { }) } +func Test_SummaryWithSpecifiedMaxAge(t *testing.T) { + summaryHelp := "Uses default value for max age" + + var metrics struct { + Summary func() prometheus.Summary `name:"without_max_age" help:"Uses default value for max age" max_age:"1ms"` + } + + err := gotoprom.Init(&metrics, "test") + assert.NoError(t, err) + + metrics.Summary().Observe(1.0) + metrics.Summary().Observe(2.0) + metrics.Summary().Observe(3.0) + + nl := "\n" + + expectedUnexpired := "" + + `# HELP test_without_max_age ` + summaryHelp + nl + + `# TYPE test_without_max_age summary` + nl + + `test_without_max_age{quantile="0.5"} 2` + nl + + `test_without_max_age{quantile="0.9"} 3` + nl + + `test_without_max_age{quantile="0.99"} 3` + nl + + `test_without_max_age_sum{} 6` + nl + + `test_without_max_age_count{} 3` + nl + + err = testutil.GatherAndCompare(prometheus.DefaultGatherer, strings.NewReader(expectedUnexpired), "test_without_max_age") + assert.NoError(t, err) + + time.Sleep(time.Millisecond) + + mfs, err := prometheus.DefaultGatherer.Gather() + assert.NoError(t, err) + + var ok bool + for _, m := range mfs { + if *m.Name == "test_without_max_age" { + for _, mm := range m.Metric { + ok = math.IsNaN(*mm.GetSummary().Quantile[1].Value) + assert.Equal(t, true, ok) + } + } + } +} + func retrieveReportedLabels(t *testing.T, metric string) map[string]string { mfs, err := prometheus.DefaultGatherer.Gather() assert.Nil(t, err) diff --git a/prometheusvanilla/builders.go b/prometheusvanilla/builders.go index db17e7e..882976f 100644 --- a/prometheusvanilla/builders.go +++ b/prometheusvanilla/builders.go @@ -5,6 +5,7 @@ import ( "reflect" "strconv" "strings" + "time" "github.com/prometheus/client_golang/prometheus" ) @@ -16,6 +17,8 @@ var ( CounterType = reflect.TypeOf((*prometheus.Counter)(nil)).Elem() // GaugeType is the type of prometheus.Gauge interface GaugeType = reflect.TypeOf((*prometheus.Gauge)(nil)).Elem() + // SummaryType is the type of prometheus.Summary interface + SummaryType = reflect.TypeOf((*prometheus.Summary)(nil)).Elem() ) // BuildCounter builds a prometheus.Counter in the given prometheus.Registerer @@ -75,6 +78,29 @@ func BuildObserver(name, help, namespace string, labelNames []string, tag reflec }, hist, nil } +// BuildSummary builds a prometheus.Summary +// The function it returns returns a prometheus.Summary type as an interface{} +func BuildSummary(name, help, namespace string, labelNames []string, tag reflect.StructTag) (func(prometheus.Labels) interface{}, prometheus.Collector, error) { + maxAge, err := maxAgeFromTag(tag) + if err != nil { + return nil, nil, fmt.Errorf("build summary %q: %s", name, err) + } + + sum := prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: name, + Help: help, + Namespace: namespace, + MaxAge: maxAge, + }, + labelNames, + ) + + return func(labels prometheus.Labels) interface{} { + return sum.With(labels) + }, sum, nil +} + func bucketsFromTag(tag reflect.StructTag) ([]float64, error) { bucketsString, ok := tag.Lookup("buckets") if !ok { @@ -97,3 +123,16 @@ func bucketsFromTag(tag reflect.StructTag) ([]float64, error) { func DefaultBuckets() []float64 { return []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25} } + +func maxAgeFromTag(tag reflect.StructTag) (time.Duration, error) { + maxAgeString, ok := tag.Lookup("max_age") + if !ok { + return 0, nil + } + var maxAgeDuration time.Duration + maxAgeDuration, err := time.ParseDuration(maxAgeString) + if err != nil { + return 0, fmt.Errorf("invalid time duration specified: %s", err) + } + return maxAgeDuration, nil +} From 5f53a6e442cb09219b5a86353582e3e955becfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?mario=2Evi=C3=B1as?= Date: Mon, 13 May 2019 18:44:48 +0200 Subject: [PATCH 2/5] Increased time in summary test max_age was set to 1ms which was too short for testing and the quantiles would have expired for the metric assertion in which there were supposed to have not expired. Now it is set to 1s and then it waits 2s for the quantiles to expire. --- metrics_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metrics_test.go b/metrics_test.go index ac4ff02..efc3e87 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -284,7 +284,7 @@ func Test_SummaryWithSpecifiedMaxAge(t *testing.T) { summaryHelp := "Uses default value for max age" var metrics struct { - Summary func() prometheus.Summary `name:"without_max_age" help:"Uses default value for max age" max_age:"1ms"` + Summary func() prometheus.Summary `name:"without_max_age" help:"Uses default value for max age" max_age:"1s"` } err := gotoprom.Init(&metrics, "test") @@ -308,7 +308,7 @@ func Test_SummaryWithSpecifiedMaxAge(t *testing.T) { err = testutil.GatherAndCompare(prometheus.DefaultGatherer, strings.NewReader(expectedUnexpired), "test_without_max_age") assert.NoError(t, err) - time.Sleep(time.Millisecond) + time.Sleep(time.Second * 2) mfs, err := prometheus.DefaultGatherer.Gather() assert.NoError(t, err) From a811240b5c544e9cd8b0de6fabbc875a066b2b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?mario=2Evi=C3=B1as?= Date: Mon, 13 May 2019 18:50:00 +0200 Subject: [PATCH 3/5] Refactored assertion in testing --- metrics_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/metrics_test.go b/metrics_test.go index efc3e87..dd5ef48 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -313,12 +313,10 @@ func Test_SummaryWithSpecifiedMaxAge(t *testing.T) { mfs, err := prometheus.DefaultGatherer.Gather() assert.NoError(t, err) - var ok bool for _, m := range mfs { if *m.Name == "test_without_max_age" { for _, mm := range m.Metric { - ok = math.IsNaN(*mm.GetSummary().Quantile[1].Value) - assert.Equal(t, true, ok) + assert.True(t, math.IsNaN(*mm.GetSummary().Quantile[1].Value)) } } } From d2767436fd296f4de5f136e1d6cd39cf2e93e690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?mario=2Evi=C3=B1as?= Date: Mon, 20 May 2019 13:56:42 +0200 Subject: [PATCH 4/5] Updated documentation to include Summary --- README.md | 4 +++- prometheusvanilla/builders.go | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a42ea7..372a70b 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ Define your metrics: var metrics struct { SomeCounter func() prometheus.Counter `name:"some_counter" help:"some counter"` SomeObserver func() prometheus.Observer `name:"some_observer" help:"Some observer with default buckets"` - SomeObserverWithSpecificBuckets func() prometheus.Observer `name:"some_observer_with_buckets" help:"Some observer with default buckets" buckets:".01,.05,.1"` + SomeObserverWithSpecificBuckets func() prometheus.Observer `name:"some_observer_with_buckets" help:"Some observer with custom buckets" buckets:".01,.05,.1"` SomeGauge func() prometheus.Gauge `name:"some_gauge" help:"Some gauge"` + SomeSummaryWithSpecificMaxAge func() prometheus.Summary `name:"some_summary_with_specific_max_age" help:"Some summary with custom max age" max_age:"20m"` Requests struct { Total func(requestLabels) prometheus.Count `name:"total" help:"Total amount of requests served"` @@ -57,6 +58,7 @@ By default, only some basic metric types are registered when `gotoprom` is intia * `prometheus.Counter` * `prometheus.Observer` * `prometheus.Gauge` +* `prometheus.Summary` You can extend this by adding more types, for instance, if you want to observe time and want to avoid repetitive code you can create a `prometheusx.TimeObserver`: diff --git a/prometheusvanilla/builders.go b/prometheusvanilla/builders.go index 882976f..66d1938 100644 --- a/prometheusvanilla/builders.go +++ b/prometheusvanilla/builders.go @@ -129,7 +129,6 @@ func maxAgeFromTag(tag reflect.StructTag) (time.Duration, error) { if !ok { return 0, nil } - var maxAgeDuration time.Duration maxAgeDuration, err := time.ParseDuration(maxAgeString) if err != nil { return 0, fmt.Errorf("invalid time duration specified: %s", err) From 26fd7f2c69460a6ec68caa10343440157aad1e14 Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 20 May 2019 19:01:26 +0200 Subject: [PATCH 5/5] Update prometheusvanilla/builders.go Fix error log Co-Authored-By: Oleg Zaytsev --- prometheusvanilla/builders.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheusvanilla/builders.go b/prometheusvanilla/builders.go index 66d1938..cfb368f 100644 --- a/prometheusvanilla/builders.go +++ b/prometheusvanilla/builders.go @@ -131,7 +131,7 @@ func maxAgeFromTag(tag reflect.StructTag) (time.Duration, error) { } maxAgeDuration, err := time.ParseDuration(maxAgeString) if err != nil { - return 0, fmt.Errorf("invalid time duration specified: %s", err) + return 0, fmt.Errorf("invalid max_age tag specified: %s", err) } return maxAgeDuration, nil }