Skip to content

Commit

Permalink
Add exemplar support for const histogram and const metric (#986)
Browse files Browse the repository at this point in the history
* Add support for exemplars on constHistogram

Co-authored-by: William Perron <william.perron@shopify.com>
Signed-off-by: William Perron <william.perron@shopify.com>

* remove GetExemplars function

Signed-off-by: William Perron <william.perron@shopify.com>

* fixed linting warnings

reduce repetition in constHistogram w/ exemplar

Signed-off-by: William Perron <william.perron@shopify.com>

* Add values to correct bucket

Signed-off-by: William Perron <william.perron@shopify.com>

* Misc fixes

Co-authored-by: Francis Bogsanyi <francis.bogsanyi@shopify.com>

Signed-off-by: William Perron <william.perron@shopify.com>

* avoid panic when there are fewer buckets than exemplars

Co-authored-by: Arun Mahendra <arun.mahendra@shopify.com>

Signed-off-by: William Perron <william.perron@shopify.com>

* Added MustNewMetricWithExemplars that wraps metrics with exemplar (#3)

Changes:
* Make sure to not "leak" dto.Metric
* Reused upper bounds we already have for histogram
* Common code for all types.

Signed-off-by: Bartlomiej Plotka <bwplotka@gmail.com>

Co-authored-by: Arun Mahendra <arun.mahendra@shopify.com>
Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
  • Loading branch information
3 people committed Mar 17, 2022
1 parent fe8d1e1 commit 66837e3
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 10 deletions.
112 changes: 110 additions & 2 deletions prometheus/examples_test.go
Expand Up @@ -24,9 +24,8 @@ import (

//nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility.
"github.com/golang/protobuf/proto"
"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/promhttp"
Expand Down Expand Up @@ -599,6 +598,115 @@ func ExampleNewConstHistogram() {
// >
}

func ExampleNewConstHistogram_WithExemplar() {
desc := prometheus.NewDesc(
"http_request_duration_seconds",
"A histogram of the HTTP request durations.",
[]string{"code", "method"},
prometheus.Labels{"owner": "example"},
)

// Create a constant histogram from values we got from a 3rd party telemetry system.
h := prometheus.MustNewConstHistogram(
desc,
4711, 403.34,
map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233},
"200", "get",
)

// Wrap const histogram with exemplars for each bucket.
exemplarTs, _ := time.Parse(time.RFC850, "Monday, 02-Jan-06 15:04:05 GMT")
exemplarLabels := prometheus.Labels{"testName": "testVal"}
h = prometheus.MustNewMetricWithExemplars(
h,
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 24.0},
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 42.0},
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 89.0},
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 157.0},
)

// Just for demonstration, let's check the state of the histogram by
// (ab)using its Write method (which is usually only used by Prometheus
// internally).
metric := &dto.Metric{}
h.Write(metric)
fmt.Println(proto.MarshalTextString(metric))

// Output:
// label: <
// name: "code"
// value: "200"
// >
// label: <
// name: "method"
// value: "get"
// >
// label: <
// name: "owner"
// value: "example"
// >
// histogram: <
// sample_count: 4711
// sample_sum: 403.34
// bucket: <
// cumulative_count: 121
// upper_bound: 25
// exemplar: <
// label: <
// name: "testName"
// value: "testVal"
// >
// value: 24
// timestamp: <
// seconds: 1136214245
// >
// >
// >
// bucket: <
// cumulative_count: 2403
// upper_bound: 50
// exemplar: <
// label: <
// name: "testName"
// value: "testVal"
// >
// value: 42
// timestamp: <
// seconds: 1136214245
// >
// >
// >
// bucket: <
// cumulative_count: 3221
// upper_bound: 100
// exemplar: <
// label: <
// name: "testName"
// value: "testVal"
// >
// value: 89
// timestamp: <
// seconds: 1136214245
// >
// >
// >
// bucket: <
// cumulative_count: 4233
// upper_bound: 200
// exemplar: <
// label: <
// name: "testName"
// value: "testVal"
// >
// value: 157
// timestamp: <
// seconds: 1136214245
// >
// >
// >
// >
}

func ExampleAlreadyRegisteredError() {
reqCounter := prometheus.NewCounter(prometheus.CounterOpts{
Name: "requests_total",
Expand Down
2 changes: 1 addition & 1 deletion prometheus/histogram.go
Expand Up @@ -581,11 +581,11 @@ func (h *constHistogram) Desc() *Desc {

func (h *constHistogram) Write(out *dto.Metric) error {
his := &dto.Histogram{}

buckets := make([]*dto.Bucket, 0, len(h.buckets))

his.SampleCount = proto.Uint64(h.count)
his.SampleSum = proto.Float64(h.sum)

for upperBound, count := range h.buckets {
buckets = append(buckets, &dto.Bucket{
CumulativeCount: proto.Uint64(count),
Expand Down
12 changes: 6 additions & 6 deletions prometheus/histogram_test.go
Expand Up @@ -424,24 +424,24 @@ func TestHistogramExemplar(t *testing.T) {
}
expectedExemplars := []*dto.Exemplar{
nil,
&dto.Exemplar{
{
Label: []*dto.LabelPair{
&dto.LabelPair{Name: proto.String("id"), Value: proto.String("2")},
{Name: proto.String("id"), Value: proto.String("2")},
},
Value: proto.Float64(1.6),
Timestamp: ts,
},
nil,
&dto.Exemplar{
{
Label: []*dto.LabelPair{
&dto.LabelPair{Name: proto.String("id"), Value: proto.String("3")},
{Name: proto.String("id"), Value: proto.String("3")},
},
Value: proto.Float64(4),
Timestamp: ts,
},
&dto.Exemplar{
{
Label: []*dto.LabelPair{
&dto.LabelPair{Name: proto.String("id"), Value: proto.String("4")},
{Name: proto.String("id"), Value: proto.String("4")},
},
Value: proto.Float64(4.5),
Timestamp: ts,
Expand Down
90 changes: 90 additions & 0 deletions prometheus/metric.go
Expand Up @@ -14,6 +14,8 @@
package prometheus

import (
"errors"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -158,3 +160,91 @@ func (m timestampedMetric) Write(pb *dto.Metric) error {
func NewMetricWithTimestamp(t time.Time, m Metric) Metric {
return timestampedMetric{Metric: m, t: t}
}

type withExemplarsMetric struct {
Metric

exemplars []*dto.Exemplar
}

func (m *withExemplarsMetric) Write(pb *dto.Metric) error {
if err := m.Metric.Write(pb); err != nil {
return err
}

switch {
case pb.Counter != nil:
pb.Counter.Exemplar = m.exemplars[len(m.exemplars)-1]
case pb.Histogram != nil:
for _, e := range m.exemplars {
// pb.Histogram.Bucket are sorted by UpperBound.
i := sort.Search(len(pb.Histogram.Bucket), func(i int) bool {
return pb.Histogram.Bucket[i].GetUpperBound() >= e.GetValue()
})
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")
}
}
default:
// TODO(bwplotka): Implement Gauge?
return errors.New("cannot inject exemplar into Gauge, Summary or Untyped")
}

return nil
}

// Exemplar is easier to use, user-facing representation of *dto.Exemplar.
type Exemplar struct {
Value float64
Labels Labels
// Optional.
// Default value (time.Time{}) indicates its empty, which should be
// understood as time.Now() time at the moment of creation of metric.
Timestamp time.Time
}

// NewMetricWithExemplars returns a new Metric wrapping the provided Metric with given
// exemplars. Exemplars are validated.
//
// Only last applicable exemplar is injected from the list.
// For example for Counter it means last exemplar is injected.
// For Histogram, it means last applicable exemplar for each bucket is injected.
//
// NewMetricWithExemplars works best with MustNewConstMetric and
// MustNewConstHistogram, see example.
func NewMetricWithExemplars(m Metric, exemplars ...Exemplar) (Metric, error) {
if len(exemplars) == 0 {
return nil, errors.New("no exemplar was passed for NewMetricWithExemplars")
}

var (
now = time.Now()
exs = make([]*dto.Exemplar, len(exemplars))
err error
)
for i, e := range exemplars {
ts := e.Timestamp
if ts == (time.Time{}) {
ts = now
}
exs[i], err = newExemplar(e.Value, ts, e.Labels)
if err != nil {
return nil, err
}
}

return &withExemplarsMetric{Metric: m, exemplars: exs}, nil
}

// MustNewMetricWithExemplars is a version of NewMetricWithExemplars that panics where
// NewMetricWithExemplars would have returned an error.
func MustNewMetricWithExemplars(m Metric, exemplars ...Exemplar) Metric {
ret, err := NewMetricWithExemplars(m, exemplars...)
if err != nil {
panic(err)
}
return ret
}
45 changes: 44 additions & 1 deletion prometheus/metric_test.go
Expand Up @@ -13,7 +13,12 @@

package prometheus

import "testing"
import (
"testing"

"github.com/golang/protobuf/proto"
dto "github.com/prometheus/client_model/go"
)

func TestBuildFQName(t *testing.T) {
scenarios := []struct{ namespace, subsystem, name, result string }{
Expand All @@ -33,3 +38,41 @@ func TestBuildFQName(t *testing.T) {
}
}
}

func TestWithExemplarsMetric(t *testing.T) {
t.Run("histogram", func(t *testing.T) {
// Create a constant histogram from values we got from a 3rd party telemetry system.
h := MustNewConstHistogram(
NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil),
4711, 403.34,
map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233},
)

m := &withExemplarsMetric{Metric: h, exemplars: []*dto.Exemplar{
{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)},
}}
metric := dto.Metric{}
if err := m.Write(&metric); err != nil {
t.Fatal(err)
}
if want, got := 4, len(metric.GetHistogram().Bucket); want != got {
t.Errorf("want %v, got %v", want, got)
}

expectedExemplarVals := []float64{24.0, 42.0, 100.0, 157.0}
for i, b := range metric.GetHistogram().Bucket {
if b.Exemplar == nil {
t.Errorf("Expected exemplar for bucket %v, got nil", i)
}
if want, got := expectedExemplarVals[i], *metric.GetHistogram().Bucket[i].Exemplar.Value; want != got {
t.Errorf("%v: want %v, got %v", i, want, got)
}
}
})

}

0 comments on commit 66837e3

Please sign in to comment.