Add exemplar support for const histogram and const metric (#986)
* 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>
This commit is contained in:
parent
fe8d1e13cd
commit
66837e3298
|
@ -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"
|
||||
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
package prometheus
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }{
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue