Skip to content

Commit 66837e3

Browse files
wperronarun-shopifybwplotka
authored
Add exemplar support for const histogram and const metric (#986)
* Add support for exemplars on constHistogram Co-authored-by: William Perron <[email protected]> Signed-off-by: William Perron <[email protected]> * remove GetExemplars function Signed-off-by: William Perron <[email protected]> * fixed linting warnings reduce repetition in constHistogram w/ exemplar Signed-off-by: William Perron <[email protected]> * Add values to correct bucket Signed-off-by: William Perron <[email protected]> * Misc fixes Co-authored-by: Francis Bogsanyi <[email protected]> Signed-off-by: William Perron <[email protected]> * avoid panic when there are fewer buckets than exemplars Co-authored-by: Arun Mahendra <[email protected]> Signed-off-by: William Perron <[email protected]> * 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 <[email protected]> Co-authored-by: Arun Mahendra <[email protected]> Co-authored-by: Bartlomiej Plotka <[email protected]>
1 parent fe8d1e1 commit 66837e3

File tree

5 files changed

+251
-10
lines changed

5 files changed

+251
-10
lines changed

prometheus/examples_test.go

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ import (
2424

2525
//nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility.
2626
"github.com/golang/protobuf/proto"
27-
"github.com/prometheus/common/expfmt"
28-
2927
dto "github.com/prometheus/client_model/go"
28+
"github.com/prometheus/common/expfmt"
3029

3130
"github.com/prometheus/client_golang/prometheus"
3231
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -599,6 +598,115 @@ func ExampleNewConstHistogram() {
599598
// >
600599
}
601600

601+
func ExampleNewConstHistogram_WithExemplar() {
602+
desc := prometheus.NewDesc(
603+
"http_request_duration_seconds",
604+
"A histogram of the HTTP request durations.",
605+
[]string{"code", "method"},
606+
prometheus.Labels{"owner": "example"},
607+
)
608+
609+
// Create a constant histogram from values we got from a 3rd party telemetry system.
610+
h := prometheus.MustNewConstHistogram(
611+
desc,
612+
4711, 403.34,
613+
map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233},
614+
"200", "get",
615+
)
616+
617+
// Wrap const histogram with exemplars for each bucket.
618+
exemplarTs, _ := time.Parse(time.RFC850, "Monday, 02-Jan-06 15:04:05 GMT")
619+
exemplarLabels := prometheus.Labels{"testName": "testVal"}
620+
h = prometheus.MustNewMetricWithExemplars(
621+
h,
622+
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 24.0},
623+
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 42.0},
624+
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 89.0},
625+
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 157.0},
626+
)
627+
628+
// Just for demonstration, let's check the state of the histogram by
629+
// (ab)using its Write method (which is usually only used by Prometheus
630+
// internally).
631+
metric := &dto.Metric{}
632+
h.Write(metric)
633+
fmt.Println(proto.MarshalTextString(metric))
634+
635+
// Output:
636+
// label: <
637+
// name: "code"
638+
// value: "200"
639+
// >
640+
// label: <
641+
// name: "method"
642+
// value: "get"
643+
// >
644+
// label: <
645+
// name: "owner"
646+
// value: "example"
647+
// >
648+
// histogram: <
649+
// sample_count: 4711
650+
// sample_sum: 403.34
651+
// bucket: <
652+
// cumulative_count: 121
653+
// upper_bound: 25
654+
// exemplar: <
655+
// label: <
656+
// name: "testName"
657+
// value: "testVal"
658+
// >
659+
// value: 24
660+
// timestamp: <
661+
// seconds: 1136214245
662+
// >
663+
// >
664+
// >
665+
// bucket: <
666+
// cumulative_count: 2403
667+
// upper_bound: 50
668+
// exemplar: <
669+
// label: <
670+
// name: "testName"
671+
// value: "testVal"
672+
// >
673+
// value: 42
674+
// timestamp: <
675+
// seconds: 1136214245
676+
// >
677+
// >
678+
// >
679+
// bucket: <
680+
// cumulative_count: 3221
681+
// upper_bound: 100
682+
// exemplar: <
683+
// label: <
684+
// name: "testName"
685+
// value: "testVal"
686+
// >
687+
// value: 89
688+
// timestamp: <
689+
// seconds: 1136214245
690+
// >
691+
// >
692+
// >
693+
// bucket: <
694+
// cumulative_count: 4233
695+
// upper_bound: 200
696+
// exemplar: <
697+
// label: <
698+
// name: "testName"
699+
// value: "testVal"
700+
// >
701+
// value: 157
702+
// timestamp: <
703+
// seconds: 1136214245
704+
// >
705+
// >
706+
// >
707+
// >
708+
}
709+
602710
func ExampleAlreadyRegisteredError() {
603711
reqCounter := prometheus.NewCounter(prometheus.CounterOpts{
604712
Name: "requests_total",

prometheus/histogram.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,11 +581,11 @@ func (h *constHistogram) Desc() *Desc {
581581

582582
func (h *constHistogram) Write(out *dto.Metric) error {
583583
his := &dto.Histogram{}
584+
584585
buckets := make([]*dto.Bucket, 0, len(h.buckets))
585586

586587
his.SampleCount = proto.Uint64(h.count)
587588
his.SampleSum = proto.Float64(h.sum)
588-
589589
for upperBound, count := range h.buckets {
590590
buckets = append(buckets, &dto.Bucket{
591591
CumulativeCount: proto.Uint64(count),

prometheus/histogram_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -424,24 +424,24 @@ func TestHistogramExemplar(t *testing.T) {
424424
}
425425
expectedExemplars := []*dto.Exemplar{
426426
nil,
427-
&dto.Exemplar{
427+
{
428428
Label: []*dto.LabelPair{
429-
&dto.LabelPair{Name: proto.String("id"), Value: proto.String("2")},
429+
{Name: proto.String("id"), Value: proto.String("2")},
430430
},
431431
Value: proto.Float64(1.6),
432432
Timestamp: ts,
433433
},
434434
nil,
435-
&dto.Exemplar{
435+
{
436436
Label: []*dto.LabelPair{
437-
&dto.LabelPair{Name: proto.String("id"), Value: proto.String("3")},
437+
{Name: proto.String("id"), Value: proto.String("3")},
438438
},
439439
Value: proto.Float64(4),
440440
Timestamp: ts,
441441
},
442-
&dto.Exemplar{
442+
{
443443
Label: []*dto.LabelPair{
444-
&dto.LabelPair{Name: proto.String("id"), Value: proto.String("4")},
444+
{Name: proto.String("id"), Value: proto.String("4")},
445445
},
446446
Value: proto.Float64(4.5),
447447
Timestamp: ts,

prometheus/metric.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
package prometheus
1515

1616
import (
17+
"errors"
18+
"sort"
1719
"strings"
1820
"time"
1921

@@ -158,3 +160,91 @@ func (m timestampedMetric) Write(pb *dto.Metric) error {
158160
func NewMetricWithTimestamp(t time.Time, m Metric) Metric {
159161
return timestampedMetric{Metric: m, t: t}
160162
}
163+
164+
type withExemplarsMetric struct {
165+
Metric
166+
167+
exemplars []*dto.Exemplar
168+
}
169+
170+
func (m *withExemplarsMetric) Write(pb *dto.Metric) error {
171+
if err := m.Metric.Write(pb); err != nil {
172+
return err
173+
}
174+
175+
switch {
176+
case pb.Counter != nil:
177+
pb.Counter.Exemplar = m.exemplars[len(m.exemplars)-1]
178+
case pb.Histogram != nil:
179+
for _, e := range m.exemplars {
180+
// pb.Histogram.Bucket are sorted by UpperBound.
181+
i := sort.Search(len(pb.Histogram.Bucket), func(i int) bool {
182+
return pb.Histogram.Bucket[i].GetUpperBound() >= e.GetValue()
183+
})
184+
if i < len(pb.Histogram.Bucket) {
185+
pb.Histogram.Bucket[i].Exemplar = e
186+
} else {
187+
// This is not possible as last bucket is Inf.
188+
panic("no bucket was found for given exemplar value")
189+
}
190+
}
191+
default:
192+
// TODO(bwplotka): Implement Gauge?
193+
return errors.New("cannot inject exemplar into Gauge, Summary or Untyped")
194+
}
195+
196+
return nil
197+
}
198+
199+
// Exemplar is easier to use, user-facing representation of *dto.Exemplar.
200+
type Exemplar struct {
201+
Value float64
202+
Labels Labels
203+
// Optional.
204+
// Default value (time.Time{}) indicates its empty, which should be
205+
// understood as time.Now() time at the moment of creation of metric.
206+
Timestamp time.Time
207+
}
208+
209+
// NewMetricWithExemplars returns a new Metric wrapping the provided Metric with given
210+
// exemplars. Exemplars are validated.
211+
//
212+
// Only last applicable exemplar is injected from the list.
213+
// For example for Counter it means last exemplar is injected.
214+
// For Histogram, it means last applicable exemplar for each bucket is injected.
215+
//
216+
// NewMetricWithExemplars works best with MustNewConstMetric and
217+
// MustNewConstHistogram, see example.
218+
func NewMetricWithExemplars(m Metric, exemplars ...Exemplar) (Metric, error) {
219+
if len(exemplars) == 0 {
220+
return nil, errors.New("no exemplar was passed for NewMetricWithExemplars")
221+
}
222+
223+
var (
224+
now = time.Now()
225+
exs = make([]*dto.Exemplar, len(exemplars))
226+
err error
227+
)
228+
for i, e := range exemplars {
229+
ts := e.Timestamp
230+
if ts == (time.Time{}) {
231+
ts = now
232+
}
233+
exs[i], err = newExemplar(e.Value, ts, e.Labels)
234+
if err != nil {
235+
return nil, err
236+
}
237+
}
238+
239+
return &withExemplarsMetric{Metric: m, exemplars: exs}, nil
240+
}
241+
242+
// MustNewMetricWithExemplars is a version of NewMetricWithExemplars that panics where
243+
// NewMetricWithExemplars would have returned an error.
244+
func MustNewMetricWithExemplars(m Metric, exemplars ...Exemplar) Metric {
245+
ret, err := NewMetricWithExemplars(m, exemplars...)
246+
if err != nil {
247+
panic(err)
248+
}
249+
return ret
250+
}

prometheus/metric_test.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313

1414
package prometheus
1515

16-
import "testing"
16+
import (
17+
"testing"
18+
19+
"github.com/golang/protobuf/proto"
20+
dto "github.com/prometheus/client_model/go"
21+
)
1722

1823
func TestBuildFQName(t *testing.T) {
1924
scenarios := []struct{ namespace, subsystem, name, result string }{
@@ -33,3 +38,41 @@ func TestBuildFQName(t *testing.T) {
3338
}
3439
}
3540
}
41+
42+
func TestWithExemplarsMetric(t *testing.T) {
43+
t.Run("histogram", func(t *testing.T) {
44+
// Create a constant histogram from values we got from a 3rd party telemetry system.
45+
h := MustNewConstHistogram(
46+
NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil),
47+
4711, 403.34,
48+
map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233},
49+
)
50+
51+
m := &withExemplarsMetric{Metric: h, exemplars: []*dto.Exemplar{
52+
{Value: proto.Float64(24.0)},
53+
{Value: proto.Float64(25.1)},
54+
{Value: proto.Float64(42.0)},
55+
{Value: proto.Float64(89.0)},
56+
{Value: proto.Float64(100.0)},
57+
{Value: proto.Float64(157.0)},
58+
}}
59+
metric := dto.Metric{}
60+
if err := m.Write(&metric); err != nil {
61+
t.Fatal(err)
62+
}
63+
if want, got := 4, len(metric.GetHistogram().Bucket); want != got {
64+
t.Errorf("want %v, got %v", want, got)
65+
}
66+
67+
expectedExemplarVals := []float64{24.0, 42.0, 100.0, 157.0}
68+
for i, b := range metric.GetHistogram().Bucket {
69+
if b.Exemplar == nil {
70+
t.Errorf("Expected exemplar for bucket %v, got nil", i)
71+
}
72+
if want, got := expectedExemplarVals[i], *metric.GetHistogram().Bucket[i].Exemplar.Value; want != got {
73+
t.Errorf("%v: want %v, got %v", i, want, got)
74+
}
75+
}
76+
})
77+
78+
}

0 commit comments

Comments
 (0)