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..efc3e87 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:"1s"` + } + + 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.Second * 2) + + 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 +}