Skip to content

Commit

Permalink
Add objectives to summaries from struct tag
Browse files Browse the repository at this point in the history
Prometheus has deprecated the default values for objectives and they now need to be explicitly specified.
Default values are those that client_golang library had before deprecation in v1.
If no objectives are specified, default objectives are used.
  • Loading branch information
mapno committed Oct 10, 2019
1 parent 7f76d2f commit b3bd774
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 15 deletions.
41 changes: 36 additions & 5 deletions prometheusvanilla/builders.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package prometheusvanilla

import (
"fmt"
"math"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -85,13 +86,14 @@ func BuildSummary(name, help, namespace string, labelNames []string, tag reflect
if err != nil {
return nil, nil, fmt.Errorf("build summary %q: %s", name, err)
}

objectives, err := objectivesFromTag(tag)
sum := prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: name,
Help: help,
Namespace: namespace,
MaxAge: maxAge,
Name: name,
Help: help,
Namespace: namespace,
MaxAge: maxAge,
Objectives: objectives,
},
labelNames,
)
Expand Down Expand Up @@ -135,3 +137,32 @@ func maxAgeFromTag(tag reflect.StructTag) (time.Duration, error) {
}
return maxAgeDuration, nil
}

func objectivesFromTag(tag reflect.StructTag) (map[float64]float64, error) {
quantileString, ok := tag.Lookup("objectives")
if !ok {
return DefaultObjectives(), nil
}
quantileSlice := strings.Split(quantileString, ",")
objectives := make(map[float64]float64, len(quantileSlice))
for _, quantile := range quantileSlice {
q, err := strconv.ParseFloat(quantile, 64)
if err != nil {
return nil, fmt.Errorf("invalid objective specified: %s", err)
}
objectives[q] = absError(q)
}
return objectives, nil
}

// DefaultObjectives provides a list of objectives you can use when you don't know what to use yet.
func DefaultObjectives() map[float64]float64 {
return map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}
}

// absError will calculate the absolute error for a given objective up to 3 decimal places.
// The variance is calculated based on the given quantile.
// Values in lower quantiles have a higher probability of being similar, so we can apply greater variances.
func absError(obj float64) float64 {
return math.Round((0.1*(1-obj))*1000) / 1000
}
62 changes: 52 additions & 10 deletions prometheusvanilla/builders_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package prometheusvanilla

import (
"fmt"
"reflect"
"testing"
"time"
Expand All @@ -16,13 +17,16 @@ const (
)

var (
labels = make(prometheus.Labels, 2)
keys = make([]string, 0, len(labels))
defaultTag reflect.StructTag = `name:"some_name" help:"some help for the metric"`
bucketsTag reflect.StructTag = `name:"some_name" help:"some help for the metric" buckets:"0.001,0.005,0.01,0.05,0.1,0.5,1,5,10"`
maxAgeTag reflect.StructTag = `name:"some_name" help:"some help for the metric" max_age:"1h"`
expectedBuckets = []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10}
expectedMaxAge = time.Hour
labels = make(prometheus.Labels, 2)
keys = make([]string, 0, len(labels))
defaultTag reflect.StructTag = `name:"some_name" help:"some help for the metric"`
bucketsTag reflect.StructTag = `name:"some_name" help:"some help for the metric" buckets:"0.001,0.005,0.01,0.05,0.1,0.5,1,5,10"`
maxAgeTag reflect.StructTag = `name:"some_name" help:"some help for the metric" max_age:"1h"`
objectivesTag reflect.StructTag = `name:"some_name" help:"some help for the metric" max_age:"1h" objectives:"0.55,0.95,0.98"`
objectivesMalformedTag reflect.StructTag = `name:"some_name" help:"some help for the metric" max_age:"1h" objectives:"notFloat"`
expectedBuckets = []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10}
expectedMaxAge = time.Hour
expectedObjectives = map[float64]float64{0.55: 0.045, 0.95: 0.005, 0.98: 0.002}
)

func TestBuilders(t *testing.T) {
Expand Down Expand Up @@ -62,7 +66,7 @@ func TestBuilders(t *testing.T) {
})

t.Run("Test building a summary with malformed max_age", func(t *testing.T) {
_, _, err := BuildSummary(name, help, nameSpace, keys, `max_age:"one year"`)
_, _, err := BuildSummary(name, help, nameSpace, keys, `max_age:"one year" objectives:"0.1,0.25"`)
assert.Error(t, err)
})
}
Expand All @@ -85,15 +89,53 @@ func TestMaxAge(t *testing.T) {
t.Run("Test it retrieves custom max_age", func(t *testing.T) {
maxAge, err := maxAgeFromTag(maxAgeTag)
assert.NoError(t, err)
assert.Equal(t, maxAge, expectedMaxAge)
assert.Equal(t, expectedMaxAge, maxAge)
})
t.Run("Test it returns 0 when no max_age is found", func(t *testing.T) {
maxAge, err := maxAgeFromTag(defaultTag)
assert.NoError(t, err)
assert.Equal(t, maxAge, time.Duration(0))
assert.Equal(t, time.Duration(0), maxAge)
})
}

func TestObjectives(t *testing.T) {
t.Run("Test parsing objectives from tag", func(t *testing.T) {
obj, err := objectivesFromTag(objectivesTag)
assert.NoError(t, err)
assert.Equal(t, expectedObjectives, obj)
})
t.Run("Test returning default objective values when none are specified", func(t *testing.T) {
obj, err := objectivesFromTag(defaultTag)
assert.NoError(t, err)
assert.Equal(t, DefaultObjectives(), obj)
})
t.Run("Test returning default objective values when none are specified", func(t *testing.T) {
obj, err := objectivesFromTag(objectivesMalformedTag)
assert.Error(t, err)
assert.Nil(t, obj)
})
}

func TestAbsError(t *testing.T) {
tests := []struct {
objective float64
absErr float64
}{
{0.5, 0.05},
{0.99, 0.001},
{0.999, 0},
{0, 0.1},
{-10, 1.1},
}

for _, test := range tests {
t.Run(fmt.Sprintf("Test calculate absolute error for obejctive %2f", test.objective), func(t *testing.T) {
e := absError(test.objective)
assert.Equal(t, test.absErr, e)
})
}
}

func initLabels() {
labels["labels1"] = "labels"
labels["labels2"] = "labels"
Expand Down

0 comments on commit b3bd774

Please sign in to comment.