Skip to content

Commit 2db5bc8

Browse files
shivanthzenbeorn7
andauthored
Add exemplars for native histograms (#1686)
Add exemplars for native histograms This enables the `withExemplarsMetric.Write` method to also add exemplars to native histograms. --------- Signed-off-by: Shivanth <[email protected]> Signed-off-by: Shivanth MP <[email protected]> Co-authored-by: Björn Rabenstein <[email protected]>
1 parent 6f2e765 commit 2db5bc8

File tree

2 files changed

+283
-7
lines changed

2 files changed

+283
-7
lines changed

prometheus/metric.go

+18-7
Original file line numberDiff line numberDiff line change
@@ -186,21 +186,31 @@ func (m *withExemplarsMetric) Write(pb *dto.Metric) error {
186186
case pb.Counter != nil:
187187
pb.Counter.Exemplar = m.exemplars[len(m.exemplars)-1]
188188
case pb.Histogram != nil:
189+
h := pb.Histogram
189190
for _, e := range m.exemplars {
190-
// pb.Histogram.Bucket are sorted by UpperBound.
191-
i := sort.Search(len(pb.Histogram.Bucket), func(i int) bool {
192-
return pb.Histogram.Bucket[i].GetUpperBound() >= e.GetValue()
191+
if (h.GetZeroThreshold() != 0 || h.GetZeroCount() != 0 ||
192+
len(h.PositiveSpan) != 0 || len(h.NegativeSpan) != 0) &&
193+
e.GetTimestamp() != nil {
194+
h.Exemplars = append(h.Exemplars, e)
195+
if len(h.Bucket) == 0 {
196+
// Don't proceed to classic buckets if there are none.
197+
continue
198+
}
199+
}
200+
// h.Bucket are sorted by UpperBound.
201+
i := sort.Search(len(h.Bucket), func(i int) bool {
202+
return h.Bucket[i].GetUpperBound() >= e.GetValue()
193203
})
194-
if i < len(pb.Histogram.Bucket) {
195-
pb.Histogram.Bucket[i].Exemplar = e
204+
if i < len(h.Bucket) {
205+
h.Bucket[i].Exemplar = e
196206
} else {
197207
// The +Inf bucket should be explicitly added if there is an exemplar for it, similar to non-const histogram logic in https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L357-L365.
198208
b := &dto.Bucket{
199-
CumulativeCount: proto.Uint64(pb.Histogram.GetSampleCount()),
209+
CumulativeCount: proto.Uint64(h.GetSampleCount()),
200210
UpperBound: proto.Float64(math.Inf(1)),
201211
Exemplar: e,
202212
}
203-
pb.Histogram.Bucket = append(pb.Histogram.Bucket, b)
213+
h.Bucket = append(h.Bucket, b)
204214
}
205215
}
206216
default:
@@ -227,6 +237,7 @@ type Exemplar struct {
227237
// Only last applicable exemplar is injected from the list.
228238
// For example for Counter it means last exemplar is injected.
229239
// For Histogram, it means last applicable exemplar for each bucket is injected.
240+
// For a Native Histogram, all valid exemplars are injected.
230241
//
231242
// NewMetricWithExemplars works best with MustNewConstMetric and
232243
// MustNewConstHistogram, see example.

prometheus/metric_test.go

+265
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
package prometheus
1515

1616
import (
17+
"errors"
18+
"fmt"
1719
"math"
1820
"testing"
21+
"time"
1922

2023
dto "github.com/prometheus/client_model/go"
2124

2225
"google.golang.org/protobuf/proto"
26+
"google.golang.org/protobuf/types/known/timestamppb"
2327
)
2428

2529
func TestBuildFQName(t *testing.T) {
@@ -90,3 +94,264 @@ func TestWithExemplarsMetric(t *testing.T) {
9094
}
9195
})
9296
}
97+
98+
func TestWithExemplarsNativeHistogramMetric(t *testing.T) {
99+
t.Run("native histogram single exemplar", func(t *testing.T) {
100+
// Create a constant histogram from values we got from a 3rd party telemetry system.
101+
h := MustNewConstNativeHistogram(
102+
NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil),
103+
10, 12.1, map[int]int64{1: 7, 2: 1, 3: 2}, map[int]int64{}, 0, 2, 0.2, time.Date(
104+
2009, 11, 17, 20, 34, 58, 651387237, time.UTC))
105+
m := &withExemplarsMetric{Metric: h, exemplars: []*dto.Exemplar{
106+
{Value: proto.Float64(2000.0), Timestamp: timestamppb.New(time.Date(2009, 11, 17, 20, 34, 58, 3243244, time.UTC))},
107+
}}
108+
metric := dto.Metric{}
109+
if err := m.Write(&metric); err != nil {
110+
t.Fatal(err)
111+
}
112+
if want, got := 1, len(metric.GetHistogram().Exemplars); want != got {
113+
t.Errorf("want %v, got %v", want, got)
114+
}
115+
116+
for _, b := range metric.GetHistogram().Bucket {
117+
if b.Exemplar != nil {
118+
t.Error("Not expecting exemplar for bucket")
119+
}
120+
}
121+
})
122+
t.Run("native histogram multiple exemplar", func(t *testing.T) {
123+
// Create a constant histogram from values we got from a 3rd party telemetry system.
124+
h := MustNewConstNativeHistogram(
125+
NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil),
126+
10, 12.1, map[int]int64{1: 7, 2: 1, 3: 2}, map[int]int64{}, 0, 2, 0.2, time.Date(
127+
2009, 11, 17, 20, 34, 58, 651387237, time.UTC))
128+
m := &withExemplarsMetric{Metric: h, exemplars: []*dto.Exemplar{
129+
{Value: proto.Float64(2000.0), Timestamp: timestamppb.New(time.Date(2009, 11, 17, 20, 34, 58, 3243244, time.UTC))},
130+
{Value: proto.Float64(1000.0), Timestamp: timestamppb.New(time.Date(2009, 11, 17, 20, 34, 59, 3243244, time.UTC))},
131+
}}
132+
metric := dto.Metric{}
133+
if err := m.Write(&metric); err != nil {
134+
t.Fatal(err)
135+
}
136+
if want, got := 2, len(metric.GetHistogram().Exemplars); want != got {
137+
t.Errorf("want %v, got %v", want, got)
138+
}
139+
140+
for _, b := range metric.GetHistogram().Bucket {
141+
if b.Exemplar != nil {
142+
t.Error("Not expecting exemplar for bucket")
143+
}
144+
}
145+
})
146+
t.Run("native histogram exemplar without timestamp", func(t *testing.T) {
147+
// Create a constant histogram from values we got from a 3rd party telemetry system.
148+
h := MustNewConstNativeHistogram(
149+
NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil),
150+
10, 12.1, map[int]int64{1: 7, 2: 1, 3: 2}, map[int]int64{}, 0, 2, 0.2, time.Date(
151+
2009, 11, 17, 20, 34, 58, 651387237, time.UTC))
152+
m := MustNewMetricWithExemplars(h, Exemplar{
153+
Value: 1000.0,
154+
})
155+
metric := dto.Metric{}
156+
if err := m.Write(&metric); err != nil {
157+
t.Fatal(err)
158+
}
159+
if want, got := 1, len(metric.GetHistogram().Exemplars); want != got {
160+
t.Errorf("want %v, got %v", want, got)
161+
}
162+
if got := metric.GetHistogram().Exemplars[0].Timestamp; got == nil {
163+
t.Errorf("Got nil timestamp")
164+
}
165+
166+
for _, b := range metric.GetHistogram().Bucket {
167+
if b.Exemplar != nil {
168+
t.Error("Not expecting exemplar for bucket")
169+
}
170+
}
171+
})
172+
t.Run("nativehistogram metric exemplars should be available in both buckets and exemplars", func(t *testing.T) {
173+
now := time.Now()
174+
tcs := []struct {
175+
Name string
176+
Count uint64
177+
Sum float64
178+
PositiveBuckets map[int]int64
179+
NegativeBuckets map[int]int64
180+
ZeroBucket uint64
181+
NativeHistogramSchema int32
182+
NativeHistogramZeroThreshold float64
183+
CreatedTimestamp time.Time
184+
Bucket []*dto.Bucket
185+
Exemplars []Exemplar
186+
Want *dto.Metric
187+
}{
188+
{
189+
Name: "test_metric",
190+
Count: 6,
191+
Sum: 7.4,
192+
PositiveBuckets: map[int]int64{
193+
0: 1, 2: 2, 4: 2,
194+
},
195+
NegativeBuckets: map[int]int64{},
196+
ZeroBucket: 1,
197+
198+
NativeHistogramSchema: 2,
199+
NativeHistogramZeroThreshold: 2.938735877055719e-39,
200+
CreatedTimestamp: now,
201+
Bucket: []*dto.Bucket{
202+
{
203+
CumulativeCount: PointOf(uint64(6)),
204+
UpperBound: PointOf(float64(1)),
205+
},
206+
{
207+
CumulativeCount: PointOf(uint64(8)),
208+
UpperBound: PointOf(float64(2)),
209+
},
210+
{
211+
CumulativeCount: PointOf(uint64(11)),
212+
UpperBound: PointOf(float64(5)),
213+
},
214+
{
215+
CumulativeCount: PointOf(uint64(13)),
216+
UpperBound: PointOf(float64(10)),
217+
},
218+
},
219+
Exemplars: []Exemplar{
220+
{
221+
Timestamp: now,
222+
Value: 10,
223+
},
224+
},
225+
Want: &dto.Metric{
226+
Histogram: &dto.Histogram{
227+
SampleCount: proto.Uint64(6),
228+
SampleSum: proto.Float64(7.4),
229+
Schema: proto.Int32(2),
230+
ZeroThreshold: proto.Float64(2.938735877055719e-39),
231+
ZeroCount: proto.Uint64(1),
232+
PositiveSpan: []*dto.BucketSpan{
233+
{Offset: proto.Int32(0), Length: proto.Uint32(5)},
234+
},
235+
PositiveDelta: []int64{1, -1, 2, -2, 2},
236+
Exemplars: []*dto.Exemplar{
237+
{
238+
Value: PointOf(float64(10)),
239+
Timestamp: timestamppb.New(now),
240+
},
241+
},
242+
Bucket: []*dto.Bucket{
243+
{
244+
CumulativeCount: PointOf(uint64(6)),
245+
UpperBound: PointOf(float64(1)),
246+
},
247+
{
248+
CumulativeCount: PointOf(uint64(8)),
249+
UpperBound: PointOf(float64(2)),
250+
},
251+
{
252+
CumulativeCount: PointOf(uint64(11)),
253+
UpperBound: PointOf(float64(5)),
254+
},
255+
{
256+
CumulativeCount: PointOf(uint64(13)),
257+
UpperBound: PointOf(float64(10)),
258+
Exemplar: &dto.Exemplar{
259+
Timestamp: timestamppb.New(now),
260+
Value: PointOf(float64(10)),
261+
},
262+
},
263+
},
264+
CreatedTimestamp: timestamppb.New(now),
265+
},
266+
},
267+
},
268+
}
269+
270+
for _, tc := range tcs {
271+
m, err := newNativeHistogramWithClassicBuckets(NewDesc(tc.Name, "None", []string{}, map[string]string{}), tc.Count, tc.Sum, tc.PositiveBuckets, tc.NegativeBuckets, tc.ZeroBucket, tc.NativeHistogramSchema, tc.NativeHistogramZeroThreshold, tc.CreatedTimestamp, tc.Bucket)
272+
if err != nil {
273+
t.Fail()
274+
}
275+
metricWithExemplar, err := NewMetricWithExemplars(m, tc.Exemplars[0])
276+
if err != nil {
277+
t.Fail()
278+
}
279+
got := &dto.Metric{}
280+
err = metricWithExemplar.Write(got)
281+
if err != nil {
282+
t.Fail()
283+
}
284+
285+
if !proto.Equal(tc.Want, got) {
286+
t.Errorf("want histogram %q, got %q", tc.Want, got)
287+
}
288+
289+
}
290+
})
291+
}
292+
293+
func PointOf[T any](value T) *T {
294+
return &value
295+
}
296+
297+
// newNativeHistogramWithClassicBuckets returns a Metric representing
298+
// a native histogram that also has classic buckets. This is for testing purposes.
299+
func newNativeHistogramWithClassicBuckets(
300+
desc *Desc,
301+
count uint64,
302+
sum float64,
303+
positiveBuckets, negativeBuckets map[int]int64,
304+
zeroBucket uint64,
305+
schema int32,
306+
zeroThreshold float64,
307+
createdTimestamp time.Time,
308+
// DummyNativeHistogram also defines buckets in the metric for testing
309+
buckets []*dto.Bucket,
310+
labelValues ...string,
311+
) (Metric, error) {
312+
if desc.err != nil {
313+
fmt.Println("error", desc.err)
314+
return nil, desc.err
315+
}
316+
if err := validateLabelValues(labelValues, len(desc.variableLabels.names)); err != nil {
317+
return nil, err
318+
}
319+
if schema > nativeHistogramSchemaMaximum || schema < nativeHistogramSchemaMinimum {
320+
return nil, errors.New("invalid native histogram schema")
321+
}
322+
if err := validateCount(sum, count, negativeBuckets, positiveBuckets, zeroBucket); err != nil {
323+
return nil, err
324+
}
325+
326+
NegativeSpan, NegativeDelta := makeBucketsFromMap(negativeBuckets)
327+
PositiveSpan, PositiveDelta := makeBucketsFromMap(positiveBuckets)
328+
ret := &constNativeHistogram{
329+
desc: desc,
330+
Histogram: dto.Histogram{
331+
CreatedTimestamp: timestamppb.New(createdTimestamp),
332+
Schema: &schema,
333+
ZeroThreshold: &zeroThreshold,
334+
SampleCount: &count,
335+
SampleSum: &sum,
336+
337+
NegativeSpan: NegativeSpan,
338+
NegativeDelta: NegativeDelta,
339+
340+
PositiveSpan: PositiveSpan,
341+
PositiveDelta: PositiveDelta,
342+
343+
ZeroCount: proto.Uint64(zeroBucket),
344+
345+
// DummyNativeHistogram also defines buckets in the metric
346+
Bucket: buckets,
347+
},
348+
labelPairs: MakeLabelPairs(desc, labelValues),
349+
}
350+
if *ret.ZeroThreshold == 0 && *ret.ZeroCount == 0 && len(ret.PositiveSpan) == 0 && len(ret.NegativeSpan) == 0 {
351+
ret.PositiveSpan = []*dto.BucketSpan{{
352+
Offset: proto.Int32(0),
353+
Length: proto.Uint32(0),
354+
}}
355+
}
356+
return ret, nil
357+
}

0 commit comments

Comments
 (0)