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.
|
//nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility.
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/prometheus/common/expfmt"
|
|
||||||
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
"github.com/prometheus/common/expfmt"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"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() {
|
func ExampleAlreadyRegisteredError() {
|
||||||
reqCounter := prometheus.NewCounter(prometheus.CounterOpts{
|
reqCounter := prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
Name: "requests_total",
|
Name: "requests_total",
|
||||||
|
|
|
@ -581,11 +581,11 @@ func (h *constHistogram) Desc() *Desc {
|
||||||
|
|
||||||
func (h *constHistogram) Write(out *dto.Metric) error {
|
func (h *constHistogram) Write(out *dto.Metric) error {
|
||||||
his := &dto.Histogram{}
|
his := &dto.Histogram{}
|
||||||
|
|
||||||
buckets := make([]*dto.Bucket, 0, len(h.buckets))
|
buckets := make([]*dto.Bucket, 0, len(h.buckets))
|
||||||
|
|
||||||
his.SampleCount = proto.Uint64(h.count)
|
his.SampleCount = proto.Uint64(h.count)
|
||||||
his.SampleSum = proto.Float64(h.sum)
|
his.SampleSum = proto.Float64(h.sum)
|
||||||
|
|
||||||
for upperBound, count := range h.buckets {
|
for upperBound, count := range h.buckets {
|
||||||
buckets = append(buckets, &dto.Bucket{
|
buckets = append(buckets, &dto.Bucket{
|
||||||
CumulativeCount: proto.Uint64(count),
|
CumulativeCount: proto.Uint64(count),
|
||||||
|
|
|
@ -424,24 +424,24 @@ func TestHistogramExemplar(t *testing.T) {
|
||||||
}
|
}
|
||||||
expectedExemplars := []*dto.Exemplar{
|
expectedExemplars := []*dto.Exemplar{
|
||||||
nil,
|
nil,
|
||||||
&dto.Exemplar{
|
{
|
||||||
Label: []*dto.LabelPair{
|
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),
|
Value: proto.Float64(1.6),
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
&dto.Exemplar{
|
{
|
||||||
Label: []*dto.LabelPair{
|
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),
|
Value: proto.Float64(4),
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
},
|
},
|
||||||
&dto.Exemplar{
|
{
|
||||||
Label: []*dto.LabelPair{
|
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),
|
Value: proto.Float64(4.5),
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
package prometheus
|
package prometheus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -158,3 +160,91 @@ func (m timestampedMetric) Write(pb *dto.Metric) error {
|
||||||
func NewMetricWithTimestamp(t time.Time, m Metric) Metric {
|
func NewMetricWithTimestamp(t time.Time, m Metric) Metric {
|
||||||
return timestampedMetric{Metric: m, t: t}
|
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
|
package prometheus
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
)
|
||||||
|
|
||||||
func TestBuildFQName(t *testing.T) {
|
func TestBuildFQName(t *testing.T) {
|
||||||
scenarios := []struct{ namespace, subsystem, name, result string }{
|
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