Skip to content

Commit 7f4d76a

Browse files
authored
Use Extrema type for Histogram min/max (#3550)
* Use Extrema type for Histogram min/max * Add case for Extrema in AssertHasAttributes * Add changes to changelog * Add NewExtrema * Add metricdatatest tests * Use getter for Extrema * Fix Extrema doc language * Correct dataset to be one word * Ensure multiple extrema are tested in a dataset
1 parent 604772d commit 7f4d76a

File tree

10 files changed

+80
-38
lines changed

10 files changed

+80
-38
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
5050
- `Int64UpDownCounter` replaces the `syncint64.UpDownCounter`
5151
- `Int64Histogram` replaces the `syncint64.Histogram`
5252
- Add `NewTracerProvider` to `go.opentelemetry.io/otel/bridge/opentracing` to create `WrapperTracer` instances from a `TracerProvider`. (#3316)
53+
- The `Extrema` type to `go.opentelemetry.io/otel/sdk/metric/metricdata` to represent min/max values and still be able to distinguish unset and zero values. (#3487)
5354
- Add the `go.opentelemetry.io/otel/semconv/v1.17.0` package.
5455
The package contains semantic conventions from the `v1.17.0` version of the OpenTelemetry specification. (#3599)
5556

@@ -106,6 +107,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
106107
The slice of `instrument.Asynchronous` parameter is now passed as a variadic argument. (#3587)
107108
- The `Callback` in `go.opentelemetry.io/otel/metric` has the added `Observer` parameter added.
108109
This new parameter is used by `Callback` implementations to observe values for asynchronous instruments instead of calling the `Observe` method of the instrument directly. (#3584)
110+
- The `Min` and `Max` fields of the `HistogramDataPoint` in `go.opentelemetry.io/otel/sdk/metric/metricdata` are now defined with the added `Extrema` type instead of a `*float64`. (#3487)
109111

110112
### Fixed
111113

exporters/otlp/otlpmetric/internal/transform/metricdata.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -174,17 +174,22 @@ func HistogramDataPoints(dPts []metricdata.HistogramDataPoint) []*mpb.HistogramD
174174
out := make([]*mpb.HistogramDataPoint, 0, len(dPts))
175175
for _, dPt := range dPts {
176176
sum := dPt.Sum
177-
out = append(out, &mpb.HistogramDataPoint{
177+
hdp := &mpb.HistogramDataPoint{
178178
Attributes: AttrIter(dPt.Attributes.Iter()),
179179
StartTimeUnixNano: uint64(dPt.StartTime.UnixNano()),
180180
TimeUnixNano: uint64(dPt.Time.UnixNano()),
181181
Count: dPt.Count,
182182
Sum: &sum,
183183
BucketCounts: dPt.BucketCounts,
184184
ExplicitBounds: dPt.Bounds,
185-
Min: dPt.Min,
186-
Max: dPt.Max,
187-
})
185+
}
186+
if v, ok := dPt.Min.Value(); ok {
187+
hdp.Min = &v
188+
}
189+
if v, ok := dPt.Max.Value(); ok {
190+
hdp.Max = &v
191+
}
192+
out = append(out, hdp)
188193
}
189194
return out
190195
}

exporters/otlp/otlpmetric/internal/transform/metricdata_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ var (
6060
Count: 30,
6161
Bounds: []float64{1, 5},
6262
BucketCounts: []uint64{0, 30, 0},
63-
Min: &minA,
64-
Max: &maxA,
63+
Min: metricdata.NewExtrema(minA),
64+
Max: metricdata.NewExtrema(maxA),
6565
Sum: sumA,
6666
}, {
6767
Attributes: bob,
@@ -70,8 +70,8 @@ var (
7070
Count: 3,
7171
Bounds: []float64{1, 5},
7272
BucketCounts: []uint64{0, 1, 2},
73-
Min: &minB,
74-
Max: &maxB,
73+
Min: metricdata.NewExtrema(minB),
74+
Max: metricdata.NewExtrema(maxB),
7575
Sum: sumB,
7676
}}
7777

sdk/metric/internal/histogram.go

+4-6
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ func (s *deltaHistogram[N]) Aggregation() metricdata.Aggregation {
156156
Sum: b.sum,
157157
}
158158
if !s.noMinMax {
159-
hdp.Min = &b.min
160-
hdp.Max = &b.max
159+
hdp.Min = metricdata.NewExtrema(b.min)
160+
hdp.Max = metricdata.NewExtrema(b.max)
161161
}
162162
h.DataPoints = append(h.DataPoints, hdp)
163163

@@ -227,10 +227,8 @@ func (s *cumulativeHistogram[N]) Aggregation() metricdata.Aggregation {
227227
Sum: b.sum,
228228
}
229229
if !s.noMinMax {
230-
// Similar to counts, make a copy.
231-
min, max := b.min, b.max
232-
hdp.Min = &min
233-
hdp.Max = &max
230+
hdp.Min = metricdata.NewExtrema(b.min)
231+
hdp.Max = metricdata.NewExtrema(b.max)
234232
}
235233
h.DataPoints = append(h.DataPoints, hdp)
236234
// TODO (#3006): This will use an unbounded amount of memory if there

sdk/metric/internal/histogram_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ func hPoint(a attribute.Set, v float64, multi uint64) metricdata.HistogramDataPo
9292
Count: multi,
9393
Bounds: bounds,
9494
BucketCounts: counts,
95-
Min: &v,
96-
Max: &v,
95+
Min: metricdata.NewExtrema(v),
96+
Max: metricdata.NewExtrema(v),
9797
Sum: v * float64(multi),
9898
}
9999
}

sdk/metric/meter_test.go

+9-11
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ func TestCallbackUnregisterConcurrency(t *testing.T) {
169169

170170
// Instruments should produce correct ResourceMetrics.
171171
func TestMeterCreatesInstruments(t *testing.T) {
172+
extrema := metricdata.NewExtrema(7.)
172173
attrs := []attribute.KeyValue{attribute.String("name", "alice")}
173-
seven := 7.0
174174
testCases := []struct {
175175
name string
176176
fn func(*testing.T, metric.Meter)
@@ -391,8 +391,8 @@ func TestMeterCreatesInstruments(t *testing.T) {
391391
Count: 1,
392392
Bounds: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
393393
BucketCounts: []uint64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
394-
Min: &seven,
395-
Max: &seven,
394+
Min: extrema,
395+
Max: extrema,
396396
Sum: 7.0,
397397
},
398398
},
@@ -455,8 +455,8 @@ func TestMeterCreatesInstruments(t *testing.T) {
455455
Count: 1,
456456
Bounds: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
457457
BucketCounts: []uint64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
458-
Min: &seven,
459-
Max: &seven,
458+
Min: extrema,
459+
Max: extrema,
460460
Sum: 7.0,
461461
},
462462
},
@@ -882,8 +882,6 @@ func TestAttributeFilter(t *testing.T) {
882882
}
883883

884884
func testAttributeFilter(temporality metricdata.Temporality) func(*testing.T) {
885-
one := 1.0
886-
two := 2.0
887885
testcases := []struct {
888886
name string
889887
register func(t *testing.T, mtr metric.Meter) error
@@ -1130,8 +1128,8 @@ func testAttributeFilter(temporality metricdata.Temporality) func(*testing.T) {
11301128
Bounds: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
11311129
BucketCounts: []uint64{0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
11321130
Count: 2,
1133-
Min: &one,
1134-
Max: &two,
1131+
Min: metricdata.NewExtrema(1.),
1132+
Max: metricdata.NewExtrema(2.),
11351133
Sum: 3.0,
11361134
},
11371135
},
@@ -1212,8 +1210,8 @@ func testAttributeFilter(temporality metricdata.Temporality) func(*testing.T) {
12121210
Bounds: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
12131211
BucketCounts: []uint64{0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
12141212
Count: 2,
1215-
Min: &one,
1216-
Max: &two,
1213+
Min: metricdata.NewExtrema(1.),
1214+
Max: metricdata.NewExtrema(2.),
12171215
Sum: 3.0,
12181216
},
12191217
},

sdk/metric/metricdata/data.go

+19-2
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,26 @@ type HistogramDataPoint struct {
122122
BucketCounts []uint64
123123

124124
// Min is the minimum value recorded. (optional)
125-
Min *float64 `json:",omitempty"`
125+
Min Extrema
126126
// Max is the maximum value recorded. (optional)
127-
Max *float64 `json:",omitempty"`
127+
Max Extrema
128128
// Sum is the sum of the values recorded.
129129
Sum float64
130130
}
131+
132+
// Extrema is the minimum or maximum value of a dataset.
133+
type Extrema struct {
134+
value float64
135+
valid bool
136+
}
137+
138+
// NewExtrema returns an Extrema set to v.
139+
func NewExtrema(v float64) Extrema {
140+
return Extrema{value: v, valid: true}
141+
}
142+
143+
// Value returns the Extrema value and true if the Extrema is defined.
144+
// Otherwise, if the Extrema is its zero-value, defined will be false.
145+
func (e Extrema) Value() (v float64, defined bool) {
146+
return e.value, e.valid
147+
}

sdk/metric/metricdata/metricdatatest/assertion.go

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Datatypes interface {
3232
metricdata.Gauge[int64] |
3333
metricdata.Histogram |
3434
metricdata.HistogramDataPoint |
35+
metricdata.Extrema |
3536
metricdata.Metrics |
3637
metricdata.ResourceMetrics |
3738
metricdata.ScopeMetrics |
@@ -92,6 +93,8 @@ func AssertEqual[T Datatypes](t *testing.T, expected, actual T, opts ...Option)
9293
r = equalHistograms(e, aIface.(metricdata.Histogram), cfg)
9394
case metricdata.HistogramDataPoint:
9495
r = equalHistogramDataPoints(e, aIface.(metricdata.HistogramDataPoint), cfg)
96+
case metricdata.Extrema:
97+
r = equalExtrema(e, aIface.(metricdata.Extrema), cfg)
9598
case metricdata.Metrics:
9699
r = equalMetrics(e, aIface.(metricdata.Metrics), cfg)
97100
case metricdata.ResourceMetrics:
@@ -152,6 +155,8 @@ func AssertHasAttributes[T Datatypes](t *testing.T, actual T, attrs ...attribute
152155
reasons = hasAttributesSum(e, attrs...)
153156
case metricdata.HistogramDataPoint:
154157
reasons = hasAttributesHistogramDataPoints(e, attrs...)
158+
case metricdata.Extrema:
159+
// Nothing to check.
155160
case metricdata.Histogram:
156161
reasons = hasAttributesHistogram(e, attrs...)
157162
case metricdata.Metrics:

sdk/metric/metricdata/metricdatatest/assertion_test.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,18 @@ var (
7373
Value: -1.0,
7474
}
7575

76-
max, min = 99.0, 3.
76+
minA = metricdata.NewExtrema(-1.)
77+
minB, maxB = metricdata.NewExtrema(3.), metricdata.NewExtrema(99.)
78+
minC = metricdata.NewExtrema(-1.)
79+
7780
histogramDataPointA = metricdata.HistogramDataPoint{
7881
Attributes: attrA,
7982
StartTime: startA,
8083
Time: endA,
8184
Count: 2,
8285
Bounds: []float64{0, 10},
8386
BucketCounts: []uint64{1, 1},
87+
Min: minA,
8488
Sum: 2,
8589
}
8690
histogramDataPointB = metricdata.HistogramDataPoint{
@@ -90,8 +94,8 @@ var (
9094
Count: 3,
9195
Bounds: []float64{0, 10, 100},
9296
BucketCounts: []uint64{1, 1, 1},
93-
Max: &max,
94-
Min: &min,
97+
Max: maxB,
98+
Min: minB,
9599
Sum: 3,
96100
}
97101
histogramDataPointC = metricdata.HistogramDataPoint{
@@ -101,6 +105,7 @@ var (
101105
Count: 2,
102106
Bounds: []float64{0, 10},
103107
BucketCounts: []uint64{1, 1},
108+
Min: minC,
104109
Sum: 2,
105110
}
106111

@@ -247,6 +252,7 @@ func TestAssertEqual(t *testing.T) {
247252
t.Run("HistogramDataPoint", testDatatype(histogramDataPointA, histogramDataPointB, equalHistogramDataPoints))
248253
t.Run("DataPointInt64", testDatatype(dataPointInt64A, dataPointInt64B, equalDataPoints[int64]))
249254
t.Run("DataPointFloat64", testDatatype(dataPointFloat64A, dataPointFloat64B, equalDataPoints[float64]))
255+
t.Run("Extrema", testDatatype(minA, minB, equalExtrema))
250256
}
251257

252258
func TestAssertEqualIgnoreTime(t *testing.T) {
@@ -261,6 +267,7 @@ func TestAssertEqualIgnoreTime(t *testing.T) {
261267
t.Run("HistogramDataPoint", testDatatypeIgnoreTime(histogramDataPointA, histogramDataPointC, equalHistogramDataPoints))
262268
t.Run("DataPointInt64", testDatatypeIgnoreTime(dataPointInt64A, dataPointInt64C, equalDataPoints[int64]))
263269
t.Run("DataPointFloat64", testDatatypeIgnoreTime(dataPointFloat64A, dataPointFloat64C, equalDataPoints[float64]))
270+
t.Run("Extrema", testDatatypeIgnoreTime(minA, minC, equalExtrema))
264271
}
265272

266273
type unknownAggregation struct {
@@ -316,6 +323,7 @@ func TestAssertAggregationsEqual(t *testing.T) {
316323
}
317324

318325
func TestAssertAttributes(t *testing.T) {
326+
AssertHasAttributes(t, minA, attribute.Bool("A", true)) // No-op, always pass.
319327
AssertHasAttributes(t, dataPointInt64A, attribute.Bool("A", true))
320328
AssertHasAttributes(t, dataPointFloat64A, attribute.Bool("A", true))
321329
AssertHasAttributes(t, gaugeInt64A, attribute.Bool("A", true))

sdk/metric/metricdata/metricdatatest/comparisons.go

+15-6
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,10 @@ func equalHistogramDataPoints(a, b metricdata.HistogramDataPoint, cfg config) (r
267267
if !equalSlices(a.BucketCounts, b.BucketCounts) {
268268
reasons = append(reasons, notEqualStr("BucketCounts", a.BucketCounts, b.BucketCounts))
269269
}
270-
if !equalPtrValues(a.Min, b.Min) {
270+
if !eqExtrema(a.Min, b.Min) {
271271
reasons = append(reasons, notEqualStr("Min", a.Min, b.Min))
272272
}
273-
if !equalPtrValues(a.Max, b.Max) {
273+
if !eqExtrema(a.Max, b.Max) {
274274
reasons = append(reasons, notEqualStr("Max", a.Max, b.Max))
275275
}
276276
if a.Sum != b.Sum {
@@ -295,12 +295,21 @@ func equalSlices[T comparable](a, b []T) bool {
295295
return true
296296
}
297297

298-
func equalPtrValues[T comparable](a, b *T) bool {
299-
if a == nil || b == nil {
300-
return a == b
298+
func equalExtrema(a, b metricdata.Extrema, _ config) (reasons []string) {
299+
if !eqExtrema(a, b) {
300+
reasons = append(reasons, notEqualStr("Extrema", a, b))
301301
}
302+
return reasons
303+
}
302304

303-
return *a == *b
305+
func eqExtrema(a, b metricdata.Extrema) bool {
306+
aV, aOk := a.Value()
307+
bV, bOk := b.Value()
308+
309+
if !aOk || !bOk {
310+
return aOk == bOk
311+
}
312+
return aV == bV
304313
}
305314

306315
func diffSlices[T any](a, b []T, equal func(T, T) bool) (extraA, extraB []T) {

0 commit comments

Comments
 (0)