Skip to content

Commit

Permalink
Merge pull request #14 from cabify/mapno/implement-prometheus-summary
Browse files Browse the repository at this point in the history
Included the implementation builder for summaries
  • Loading branch information
mapno authored May 20, 2019
2 parents d5533bd + 26fd7f2 commit 827806c
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 6 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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`:
Expand Down
1 change: 1 addition & 0 deletions default.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 51 additions & 4 deletions metrics_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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")
Expand All @@ -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)
}

Expand Down Expand Up @@ -275,6 +280,48 @@ 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)

for _, m := range mfs {
if *m.Name == "test_without_max_age" {
for _, mm := range m.Metric {
assert.True(t, math.IsNaN(*mm.GetSummary().Quantile[1].Value))
}
}
}
}

func retrieveReportedLabels(t *testing.T, metric string) map[string]string {
mfs, err := prometheus.DefaultGatherer.Gather()
assert.Nil(t, err)
Expand Down
38 changes: 38 additions & 0 deletions prometheusvanilla/builders.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"reflect"
"strconv"
"strings"
"time"

"github.com/prometheus/client_golang/prometheus"
)
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -97,3 +123,15 @@ 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
}
maxAgeDuration, err := time.ParseDuration(maxAgeString)
if err != nil {
return 0, fmt.Errorf("invalid max_age tag specified: %s", err)
}
return maxAgeDuration, nil
}

0 comments on commit 827806c

Please sign in to comment.