From d7f6f6f76834b6528ad2a980c1887162568bbf40 Mon Sep 17 00:00:00 2001 From: Arun Mahendra Date: Fri, 21 Jan 2022 16:41:16 -0500 Subject: [PATCH 1/7] Add support for exemplars on constHistogram Co-authored-by: William Perron Signed-off-by: William Perron --- prometheus/examples_test.go | 59 ++++++++++++++++++++++++++++++++++++- prometheus/histogram.go | 59 ++++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index a73ed184c..6645de56b 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -16,6 +16,7 @@ package prometheus_test import ( "bytes" "fmt" + "google.golang.org/protobuf/types/known/timestamppb" "math" "net/http" "runtime" @@ -549,11 +550,27 @@ func ExampleNewConstHistogram() { prometheus.Labels{"owner": "example"}, ) + var exemplars []*dto.Exemplar + n := "testName" + v := "testVal" + lp := dto.LabelPair{Name: &n, Value: &v} + var labelPairs []*dto.LabelPair + labelPairs = append(labelPairs, &lp) + val := float64(42) + t, _ := time.Parse("unix", "Mon Jan _2 15:04:05 MST 2006") + ts := timestamppb.New(t) + + for i := 0; i < 4; i++ { + e := dto.Exemplar{Label: labelPairs, Value: &val, Timestamp: ts} + exemplars = append(exemplars, &e) + } + // Create a constant histogram from values we got from a 3rd party telemetry system. - h := prometheus.MustNewConstHistogram( + h := prometheus.MustNewConstHistogramWithExemplar( desc, 4711, 403.34, map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233}, + exemplars, "200", "get", ) @@ -583,18 +600,58 @@ func ExampleNewConstHistogram() { // bucket: < // cumulative_count: 121 // upper_bound: 25 + // exemplar: < + // label: < + // name: "testName" + // value: "testVal" + // > + // value: 42 + // timestamp: < + // seconds: -62135596800 + // > + // > // > // bucket: < // cumulative_count: 2403 // upper_bound: 50 + // exemplar: < + // label: < + // name: "testName" + // value: "testVal" + // > + // value: 42 + // timestamp: < + // seconds: -62135596800 + // > + // > // > // bucket: < // cumulative_count: 3221 // upper_bound: 100 + // exemplar: < + // label: < + // name: "testName" + // value: "testVal" + // > + // value: 42 + // timestamp: < + // seconds: -62135596800 + // > + // > // > // bucket: < // cumulative_count: 4233 // upper_bound: 200 + // exemplar: < + // label: < + // name: "testName" + // value: "testVal" + // > + // value: 42 + // timestamp: < + // seconds: -62135596800 + // > + // > // > // > } diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 893802fd6..b4440746d 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -573,6 +573,7 @@ type constHistogram struct { sum float64 buckets map[float64]uint64 labelPairs []*dto.LabelPair + exemplars []*dto.Exemplar } func (h *constHistogram) Desc() *Desc { @@ -585,7 +586,6 @@ func (h *constHistogram) Write(out *dto.Metric) error { 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), @@ -596,6 +596,13 @@ func (h *constHistogram) Write(out *dto.Metric) error { if len(buckets) > 0 { sort.Sort(buckSort(buckets)) } + + if len(h.exemplars) > 0 { + for i := 0; i < len(buckets); i++ { + buckets[i].Exemplar = h.exemplars[i] + } + } + his.Bucket = buckets out.Histogram = his @@ -604,6 +611,13 @@ func (h *constHistogram) Write(out *dto.Metric) error { return nil } +func (h *constHistogram) GetExemplars() []*dto.Exemplar { + if h != nil { + return h.exemplars + } + return nil +} + // NewConstHistogram returns a metric representing a Prometheus histogram with // fixed values for the count, sum, and bucket counts. As those parameters // cannot be changed, the returned value does not implement the Histogram @@ -617,6 +631,7 @@ func (h *constHistogram) Write(out *dto.Metric) error { // // NewConstHistogram returns an error if the length of labelValues is not // consistent with the variable labels in Desc or if Desc is invalid. + func NewConstHistogram( desc *Desc, count uint64, @@ -655,6 +670,48 @@ func MustNewConstHistogram( return m } +func NewConstHistogramWithExemplar( + desc *Desc, + count uint64, + sum float64, + buckets map[float64]uint64, + exemplars []*dto.Exemplar, + labelValues ...string, + +) (Metric, error) { + if desc.err != nil { + return nil, desc.err + } + if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil { + return nil, err + } + return &constHistogram{ + desc: desc, + count: count, + sum: sum, + buckets: buckets, + exemplars: exemplars, + labelPairs: MakeLabelPairs(desc, labelValues), + }, nil +} + +// MustNewConstHistogram is a version of NewConstHistogram that panics where +// NewConstHistogram would have returned an error. +func MustNewConstHistogramWithExemplar( + desc *Desc, + count uint64, + sum float64, + buckets map[float64]uint64, + exemplars []*dto.Exemplar, + labelValues ...string, +) Metric { + m, err := NewConstHistogramWithExemplar(desc, count, sum, buckets, exemplars, labelValues...) + if err != nil { + panic(err) + } + return m +} + type buckSort []*dto.Bucket func (s buckSort) Len() int { From 3d744ced0647d49be074fb1e23721fdbf821ab44 Mon Sep 17 00:00:00 2001 From: William Perron Date: Tue, 15 Feb 2022 08:10:43 -0500 Subject: [PATCH 2/7] remove GetExemplars function Signed-off-by: William Perron --- prometheus/histogram.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/prometheus/histogram.go b/prometheus/histogram.go index b4440746d..af434ae64 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -611,13 +611,6 @@ func (h *constHistogram) Write(out *dto.Metric) error { return nil } -func (h *constHistogram) GetExemplars() []*dto.Exemplar { - if h != nil { - return h.exemplars - } - return nil -} - // NewConstHistogram returns a metric representing a Prometheus histogram with // fixed values for the count, sum, and bucket counts. As those parameters // cannot be changed, the returned value does not implement the Histogram From ad6f1f6fdb92f4abafcd52cf8d9ba94f77cd3451 Mon Sep 17 00:00:00 2001 From: William Perron Date: Tue, 15 Feb 2022 10:21:52 -0500 Subject: [PATCH 3/7] fixed linting warnings reduce repetition in constHistogram w/ exemplar Signed-off-by: William Perron --- prometheus/histogram.go | 25 ++++++++++++------------- prometheus/histogram_test.go | 12 ++++++------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/prometheus/histogram.go b/prometheus/histogram.go index af434ae64..6e4cda7b7 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -678,14 +678,15 @@ func NewConstHistogramWithExemplar( if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil { return nil, err } - return &constHistogram{ - desc: desc, - count: count, - sum: sum, - buckets: buckets, - exemplars: exemplars, - labelPairs: MakeLabelPairs(desc, labelValues), - }, nil + + h, err := NewConstHistogram(desc, count, sum, buckets, labelValues...) + if err != nil { + return nil, err + } + + h.(*constHistogram).exemplars = exemplars + + return h, nil } // MustNewConstHistogram is a version of NewConstHistogram that panics where @@ -698,11 +699,9 @@ func MustNewConstHistogramWithExemplar( exemplars []*dto.Exemplar, labelValues ...string, ) Metric { - m, err := NewConstHistogramWithExemplar(desc, count, sum, buckets, exemplars, labelValues...) - if err != nil { - panic(err) - } - return m + h := MustNewConstHistogram(desc, count, sum, buckets, labelValues...) + h.(*constHistogram).exemplars = exemplars + return h } type buckSort []*dto.Bucket diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go index da5771544..b96eff9e1 100644 --- a/prometheus/histogram_test.go +++ b/prometheus/histogram_test.go @@ -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, From 47d3c542b3c985cacb59ba6d46da64197d015905 Mon Sep 17 00:00:00 2001 From: William Perron Date: Mon, 7 Mar 2022 09:19:33 -0500 Subject: [PATCH 4/7] Add values to correct bucket Signed-off-by: William Perron --- prometheus/examples_test.go | 13 +++++++------ prometheus/histogram.go | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index 6645de56b..e39b879ff 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -16,13 +16,14 @@ package prometheus_test import ( "bytes" "fmt" - "google.golang.org/protobuf/types/known/timestamppb" "math" "net/http" "runtime" "strings" "time" + "google.golang.org/protobuf/types/known/timestamppb" + //nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/prometheus/common/expfmt" @@ -556,12 +557,12 @@ func ExampleNewConstHistogram() { lp := dto.LabelPair{Name: &n, Value: &v} var labelPairs []*dto.LabelPair labelPairs = append(labelPairs, &lp) - val := float64(42) + vals := []float64{24.0, 42.0, 89.0, 157.0} t, _ := time.Parse("unix", "Mon Jan _2 15:04:05 MST 2006") ts := timestamppb.New(t) for i := 0; i < 4; i++ { - e := dto.Exemplar{Label: labelPairs, Value: &val, Timestamp: ts} + e := dto.Exemplar{Label: labelPairs, Value: &vals[i], Timestamp: ts} exemplars = append(exemplars, &e) } @@ -605,7 +606,7 @@ func ExampleNewConstHistogram() { // name: "testName" // value: "testVal" // > - // value: 42 + // value: 24 // timestamp: < // seconds: -62135596800 // > @@ -633,7 +634,7 @@ func ExampleNewConstHistogram() { // name: "testName" // value: "testVal" // > - // value: 42 + // value: 89 // timestamp: < // seconds: -62135596800 // > @@ -647,7 +648,7 @@ func ExampleNewConstHistogram() { // name: "testName" // value: "testVal" // > - // value: 42 + // value: 157 // timestamp: < // seconds: -62135596800 // > diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 6e4cda7b7..453fb8378 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -582,7 +582,9 @@ func (h *constHistogram) Desc() *Desc { func (h *constHistogram) Write(out *dto.Metric) error { his := &dto.Histogram{} + // h.buckets, buckets and bounds are all the same length buckets := make([]*dto.Bucket, 0, len(h.buckets)) + bounds := make([]float64, 0) his.SampleCount = proto.Uint64(h.count) his.SampleSum = proto.Float64(h.sum) @@ -591,15 +593,24 @@ func (h *constHistogram) Write(out *dto.Metric) error { CumulativeCount: proto.Uint64(count), UpperBound: proto.Float64(upperBound), }) + bounds = append(bounds, upperBound) } + // make sure that both bounds and buckets have the same ordering if len(buckets) > 0 { sort.Sort(buckSort(buckets)) } + sort.Float64s(bounds) if len(h.exemplars) > 0 { - for i := 0; i < len(buckets); i++ { - buckets[i].Exemplar = h.exemplars[i] + r := len(buckets) + for i := 0; i < r; i++ { + bound := sort.SearchFloat64s(bounds, *h.exemplars[i].Value) + // Only append the exemplar if it's within the bounds defined in the + // buckets. + if bound < r { + buckets[bound].Exemplar = h.exemplars[i] + } } } From 16ac1292c7437c2fbb62f48380e11f65578eb2c6 Mon Sep 17 00:00:00 2001 From: William Perron Date: Fri, 11 Mar 2022 16:07:07 -0500 Subject: [PATCH 5/7] Misc fixes Co-authored-by: Francis Bogsanyi Signed-off-by: William Perron --- prometheus/histogram.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 453fb8378..214d96832 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -584,7 +584,7 @@ func (h *constHistogram) Write(out *dto.Metric) error { his := &dto.Histogram{} // h.buckets, buckets and bounds are all the same length buckets := make([]*dto.Bucket, 0, len(h.buckets)) - bounds := make([]float64, 0) + bounds := make([]float64, 0, len(h.buckets)) his.SampleCount = proto.Uint64(h.count) his.SampleSum = proto.Float64(h.sum) @@ -599,8 +599,8 @@ func (h *constHistogram) Write(out *dto.Metric) error { // make sure that both bounds and buckets have the same ordering if len(buckets) > 0 { sort.Sort(buckSort(buckets)) + sort.Float64s(bounds) } - sort.Float64s(bounds) if len(h.exemplars) > 0 { r := len(buckets) From daaf45b2ed492ecb3dc13038819f2513998d400f Mon Sep 17 00:00:00 2001 From: Arun Mahendra Date: Wed, 9 Mar 2022 11:06:20 -0500 Subject: [PATCH 6/7] avoid panic when there are fewer buckets than exemplars Co-authored-by: Arun Mahendra Signed-off-by: William Perron --- prometheus/histogram.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 214d96832..9bcbf4610 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -604,7 +604,8 @@ func (h *constHistogram) Write(out *dto.Metric) error { if len(h.exemplars) > 0 { r := len(buckets) - for i := 0; i < r; i++ { + l := len(h.exemplars) + for i := 0; i < r && i < l; i++ { bound := sort.SearchFloat64s(bounds, *h.exemplars[i].Value) // Only append the exemplar if it's within the bounds defined in the // buckets. From 3c2e89060a5d58d143563ac002e876d95d103174 Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Wed, 16 Mar 2022 20:05:26 +0000 Subject: [PATCH 7/7] 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 --- prometheus/examples_test.go | 98 ++++++++++++++++++++++++++++--------- prometheus/histogram.go | 63 +----------------------- prometheus/metric.go | 90 ++++++++++++++++++++++++++++++++++ prometheus/metric_test.go | 45 ++++++++++++++++- 4 files changed, 209 insertions(+), 87 deletions(-) diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index e39b879ff..a7a8d0c14 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -22,13 +22,10 @@ import ( "strings" "time" - "google.golang.org/protobuf/types/known/timestamppb" - //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" @@ -551,30 +548,83 @@ func ExampleNewConstHistogram() { prometheus.Labels{"owner": "example"}, ) - var exemplars []*dto.Exemplar - n := "testName" - v := "testVal" - lp := dto.LabelPair{Name: &n, Value: &v} - var labelPairs []*dto.LabelPair - labelPairs = append(labelPairs, &lp) - vals := []float64{24.0, 42.0, 89.0, 157.0} - t, _ := time.Parse("unix", "Mon Jan _2 15:04:05 MST 2006") - ts := timestamppb.New(t) - - for i := 0; i < 4; i++ { - e := dto.Exemplar{Label: labelPairs, Value: &vals[i], Timestamp: ts} - exemplars = append(exemplars, &e) - } + // 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", + ) + + // 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 + // > + // bucket: < + // cumulative_count: 2403 + // upper_bound: 50 + // > + // bucket: < + // cumulative_count: 3221 + // upper_bound: 100 + // > + // bucket: < + // cumulative_count: 4233 + // upper_bound: 200 + // > + // > +} + +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.MustNewConstHistogramWithExemplar( + h := prometheus.MustNewConstHistogram( desc, 4711, 403.34, map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233}, - exemplars, "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). @@ -608,7 +658,7 @@ func ExampleNewConstHistogram() { // > // value: 24 // timestamp: < - // seconds: -62135596800 + // seconds: 1136214245 // > // > // > @@ -622,7 +672,7 @@ func ExampleNewConstHistogram() { // > // value: 42 // timestamp: < - // seconds: -62135596800 + // seconds: 1136214245 // > // > // > @@ -636,7 +686,7 @@ func ExampleNewConstHistogram() { // > // value: 89 // timestamp: < - // seconds: -62135596800 + // seconds: 1136214245 // > // > // > @@ -650,7 +700,7 @@ func ExampleNewConstHistogram() { // > // value: 157 // timestamp: < - // seconds: -62135596800 + // seconds: 1136214245 // > // > // > diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 9bcbf4610..0d47fecdc 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -573,7 +573,6 @@ type constHistogram struct { sum float64 buckets map[float64]uint64 labelPairs []*dto.LabelPair - exemplars []*dto.Exemplar } func (h *constHistogram) Desc() *Desc { @@ -582,9 +581,8 @@ func (h *constHistogram) Desc() *Desc { func (h *constHistogram) Write(out *dto.Metric) error { his := &dto.Histogram{} - // h.buckets, buckets and bounds are all the same length + buckets := make([]*dto.Bucket, 0, len(h.buckets)) - bounds := make([]float64, 0, len(h.buckets)) his.SampleCount = proto.Uint64(h.count) his.SampleSum = proto.Float64(h.sum) @@ -593,28 +591,11 @@ func (h *constHistogram) Write(out *dto.Metric) error { CumulativeCount: proto.Uint64(count), UpperBound: proto.Float64(upperBound), }) - bounds = append(bounds, upperBound) } - // make sure that both bounds and buckets have the same ordering if len(buckets) > 0 { sort.Sort(buckSort(buckets)) - sort.Float64s(bounds) - } - - if len(h.exemplars) > 0 { - r := len(buckets) - l := len(h.exemplars) - for i := 0; i < r && i < l; i++ { - bound := sort.SearchFloat64s(bounds, *h.exemplars[i].Value) - // Only append the exemplar if it's within the bounds defined in the - // buckets. - if bound < r { - buckets[bound].Exemplar = h.exemplars[i] - } - } } - his.Bucket = buckets out.Histogram = his @@ -636,7 +617,6 @@ func (h *constHistogram) Write(out *dto.Metric) error { // // NewConstHistogram returns an error if the length of labelValues is not // consistent with the variable labels in Desc or if Desc is invalid. - func NewConstHistogram( desc *Desc, count uint64, @@ -675,47 +655,6 @@ func MustNewConstHistogram( return m } -func NewConstHistogramWithExemplar( - desc *Desc, - count uint64, - sum float64, - buckets map[float64]uint64, - exemplars []*dto.Exemplar, - labelValues ...string, - -) (Metric, error) { - if desc.err != nil { - return nil, desc.err - } - if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil { - return nil, err - } - - h, err := NewConstHistogram(desc, count, sum, buckets, labelValues...) - if err != nil { - return nil, err - } - - h.(*constHistogram).exemplars = exemplars - - return h, nil -} - -// MustNewConstHistogram is a version of NewConstHistogram that panics where -// NewConstHistogram would have returned an error. -func MustNewConstHistogramWithExemplar( - desc *Desc, - count uint64, - sum float64, - buckets map[float64]uint64, - exemplars []*dto.Exemplar, - labelValues ...string, -) Metric { - h := MustNewConstHistogram(desc, count, sum, buckets, labelValues...) - h.(*constHistogram).exemplars = exemplars - return h -} - type buckSort []*dto.Bucket func (s buckSort) Len() int { diff --git a/prometheus/metric.go b/prometheus/metric.go index 118a54e84..48d4a5d50 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -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 +} diff --git a/prometheus/metric_test.go b/prometheus/metric_test.go index 7145f5e53..6100e0d8a 100644 --- a/prometheus/metric_test.go +++ b/prometheus/metric_test.go @@ -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) + } + } + }) + +}