From b757c7083baeece1be36caa4b1c102a3ddcdfa4b Mon Sep 17 00:00:00 2001 From: Aaron Clawson <3766680+MadVikingGod@users.noreply.github.com> Date: Wed, 14 Jun 2023 09:36:13 -0500 Subject: [PATCH] Exponential Histogram Datatypes (#4165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds the Exponential histogram data type. * Changelog * Updated comments * Apply suggestions from code review Co-authored-by: Robert Pająk Co-authored-by: Tyler Yahn * Split Exponential Buckets into it's own type --------- Co-authored-by: Robert Pająk Co-authored-by: Tyler Yahn --- CHANGELOG.md | 1 + sdk/metric/metricdata/data.go | 68 +++++++ .../metricdata/metricdatatest/assertion.go | 27 ++- .../metricdatatest/assertion_test.go | 174 ++++++++++++++++++ .../metricdata/metricdatatest/comparisons.go | 138 ++++++++++++++ 5 files changed, 407 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 798af635a..e68d9fc05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ See our [versioning policy](VERSIONING.md) for more information about these stab The package contains semantic conventions from the `v1.19.0` version of the OpenTelemetry specification. (#3848) - The `go.opentelemetry.io/otel/semconv/v1.20.0` package. The package contains semantic conventions from the `v1.20.0` version of the OpenTelemetry specification. (#4078) +- The Exponential Histogram data types in `go.opentelemetry.io/otel/sdk/metric/metricdata`. (#4165) ### Changed diff --git a/sdk/metric/metricdata/data.go b/sdk/metric/metricdata/data.go index 1e32f5eeb..49bbc0414 100644 --- a/sdk/metric/metricdata/data.go +++ b/sdk/metric/metricdata/data.go @@ -137,6 +137,74 @@ type HistogramDataPoint[N int64 | float64] struct { Exemplars []Exemplar[N] `json:",omitempty"` } +// ExponentialHistogram represents the histogram of all measurements of values from an instrument. +type ExponentialHistogram[N int64 | float64] struct { + // DataPoints are the individual aggregated measurements with unique + // attributes. + DataPoints []ExponentialHistogramDataPoint[N] + // Temporality describes if the aggregation is reported as the change from the + // last report time, or the cumulative changes since a fixed start time. + Temporality Temporality +} + +func (ExponentialHistogram[N]) privateAggregation() {} + +// ExponentialHistogramDataPoint is a single exponential histogram data point in a timeseries. +type ExponentialHistogramDataPoint[N int64 | float64] struct { + // Attributes is the set of key value pairs that uniquely identify the + // timeseries. + Attributes attribute.Set + // StartTime is when the timeseries was started. + StartTime time.Time + // Time is the time when the timeseries was recorded. + Time time.Time + + // Count is the number of updates this histogram has been calculated with. + Count uint64 + // Min is the minimum value recorded. (optional) + Min Extrema[N] + // Max is the maximum value recorded. (optional) + Max Extrema[N] + // Sum is the sum of the values recorded. + Sum N + + // Scale describes the resolution of the histogram. Boundaries are + // located at powers of the base, where: + // + // base = 2 ^ (2 ^ -Scale) + Scale int32 + // ZeroCount is the number of values whose absolute value + // is less than or equal to [ZeroThreshold]. + // When ZeroThreshold is 0, this is the number of values that + // cannot be expressed using the standard exponential formula + // as well as values that have been rounded to zero. + // ZeroCount represents the special zero count bucket. + ZeroCount uint64 + + // PositiveBucket is range of positive value bucket counts. + PositiveBucket ExponentialBucket + // NegativeBucket is range of negative value bucket counts. + NegativeBucket ExponentialBucket + + // ZeroThreshold is the width of the zero region. Where the zero region is + // defined as the closed interval [-ZeroThreshold, ZeroThreshold]. + ZeroThreshold float64 + + // Exemplars is the sampled Exemplars collected during the timeseries. + Exemplars []Exemplar[N] `json:",omitempty"` +} + +// ExponentialBucket are a set of bucket counts, encoded in a contiguous array +// of counts. +type ExponentialBucket struct { + // Offset is the bucket index of the first entry in the Counts slice. + Offset int32 + // Counts is an slice where Counts[i] carries the count of the bucket at + // index (Offset+i). Counts[i] is the count of values greater than + // base^(Offset+i) and less than or equal to base^(Offset+i+1). + Counts []uint64 +} + // Extrema is the minimum or maximum value of a dataset. type Extrema[N int64 | float64] struct { value N diff --git a/sdk/metric/metricdata/metricdatatest/assertion.go b/sdk/metric/metricdata/metricdatatest/assertion.go index 08bac5131..f559a6432 100644 --- a/sdk/metric/metricdata/metricdatatest/assertion.go +++ b/sdk/metric/metricdata/metricdatatest/assertion.go @@ -42,7 +42,12 @@ type Datatypes interface { metricdata.Sum[float64] | metricdata.Sum[int64] | metricdata.Exemplar[float64] | - metricdata.Exemplar[int64] + metricdata.Exemplar[int64] | + metricdata.ExponentialHistogram[float64] | + metricdata.ExponentialHistogram[int64] | + metricdata.ExponentialHistogramDataPoint[float64] | + metricdata.ExponentialHistogramDataPoint[int64] | + metricdata.ExponentialBucket // Interface types are not allowed in union types, therefore the // Aggregation and Value type from metricdata are not included here. @@ -134,6 +139,16 @@ func AssertEqual[T Datatypes](t *testing.T, expected, actual T, opts ...Option) r = equalSums(e, aIface.(metricdata.Sum[int64]), cfg) case metricdata.Sum[float64]: r = equalSums(e, aIface.(metricdata.Sum[float64]), cfg) + case metricdata.ExponentialHistogram[float64]: + r = equalExponentialHistograms(e, aIface.(metricdata.ExponentialHistogram[float64]), cfg) + case metricdata.ExponentialHistogram[int64]: + r = equalExponentialHistograms(e, aIface.(metricdata.ExponentialHistogram[int64]), cfg) + case metricdata.ExponentialHistogramDataPoint[float64]: + r = equalExponentialHistogramDataPoints(e, aIface.(metricdata.ExponentialHistogramDataPoint[float64]), cfg) + case metricdata.ExponentialHistogramDataPoint[int64]: + r = equalExponentialHistogramDataPoints(e, aIface.(metricdata.ExponentialHistogramDataPoint[int64]), cfg) + case metricdata.ExponentialBucket: + r = equalExponentialBuckets(e, aIface.(metricdata.ExponentialBucket), cfg) default: // We control all types passed to this, panic to signal developers // early they changed things in an incompatible way. @@ -198,6 +213,16 @@ func AssertHasAttributes[T Datatypes](t *testing.T, actual T, attrs ...attribute reasons = hasAttributesScopeMetrics(e, attrs...) case metricdata.ResourceMetrics: reasons = hasAttributesResourceMetrics(e, attrs...) + case metricdata.ExponentialHistogram[int64]: + reasons = hasAttributesExponentialHistogram(e, attrs...) + case metricdata.ExponentialHistogram[float64]: + reasons = hasAttributesExponentialHistogram(e, attrs...) + case metricdata.ExponentialHistogramDataPoint[int64]: + reasons = hasAttributesExponentialHistogramDataPoints(e, attrs...) + case metricdata.ExponentialHistogramDataPoint[float64]: + reasons = hasAttributesExponentialHistogramDataPoints(e, attrs...) + case metricdata.ExponentialBucket: + // Nothing to check. default: // We control all types passed to this, panic to signal developers // early they changed things in an incompatible way. diff --git a/sdk/metric/metricdata/metricdatatest/assertion_test.go b/sdk/metric/metricdata/metricdatatest/assertion_test.go index 6ad40ebf1..3b9d8d6da 100644 --- a/sdk/metric/metricdata/metricdatatest/assertion_test.go +++ b/sdk/metric/metricdata/metricdatatest/assertion_test.go @@ -205,6 +205,103 @@ var ( Exemplars: []metricdata.Exemplar[float64]{exemplarFloat64C}, } + exponentialBucket2 = metricdata.ExponentialBucket{ + Offset: 2, + Counts: []uint64{1, 1}, + } + exponentialBucket3 = metricdata.ExponentialBucket{ + Offset: 3, + Counts: []uint64{1, 1}, + } + exponentialBucket4 = metricdata.ExponentialBucket{ + Offset: 4, + Counts: []uint64{1, 1, 1}, + } + exponentialBucket5 = metricdata.ExponentialBucket{ + Offset: 5, + Counts: []uint64{1, 1, 1}, + } + exponentialHistogramDataPointInt64A = metricdata.ExponentialHistogramDataPoint[int64]{ + Attributes: attrA, + StartTime: startA, + Time: endA, + Count: 5, + Min: minInt64A, + Sum: 2, + Scale: 1, + ZeroCount: 1, + PositiveBucket: exponentialBucket3, + NegativeBucket: exponentialBucket2, + Exemplars: []metricdata.Exemplar[int64]{exemplarInt64A}, + } + exponentialHistogramDataPointFloat64A = metricdata.ExponentialHistogramDataPoint[float64]{ + Attributes: attrA, + StartTime: startA, + Time: endA, + Count: 5, + Min: minFloat64A, + Sum: 2, + Scale: 1, + ZeroCount: 1, + PositiveBucket: exponentialBucket3, + NegativeBucket: exponentialBucket2, + Exemplars: []metricdata.Exemplar[float64]{exemplarFloat64A}, + } + exponentialHistogramDataPointInt64B = metricdata.ExponentialHistogramDataPoint[int64]{ + Attributes: attrB, + StartTime: startB, + Time: endB, + Count: 6, + Min: minInt64B, + Max: maxInt64B, + Sum: 3, + Scale: 2, + ZeroCount: 3, + PositiveBucket: exponentialBucket4, + NegativeBucket: exponentialBucket5, + Exemplars: []metricdata.Exemplar[int64]{exemplarInt64B}, + } + exponentialHistogramDataPointFloat64B = metricdata.ExponentialHistogramDataPoint[float64]{ + Attributes: attrB, + StartTime: startB, + Time: endB, + Count: 6, + Min: minFloat64B, + Max: maxFloat64B, + Sum: 3, + Scale: 2, + ZeroCount: 3, + PositiveBucket: exponentialBucket4, + NegativeBucket: exponentialBucket5, + Exemplars: []metricdata.Exemplar[float64]{exemplarFloat64B}, + } + exponentialHistogramDataPointInt64C = metricdata.ExponentialHistogramDataPoint[int64]{ + Attributes: attrA, + StartTime: startB, + Time: endB, + Count: 5, + Min: minInt64C, + Sum: 2, + Scale: 1, + ZeroCount: 1, + PositiveBucket: exponentialBucket3, + NegativeBucket: exponentialBucket2, + Exemplars: []metricdata.Exemplar[int64]{exemplarInt64C}, + } + exponentialHistogramDataPointFloat64C = metricdata.ExponentialHistogramDataPoint[float64]{ + Attributes: attrA, + StartTime: startB, + Time: endB, + Count: 5, + Min: minFloat64A, + Sum: 2, + Scale: 1, + ZeroCount: 1, + PositiveBucket: exponentialBucket3, + NegativeBucket: exponentialBucket2, + Exemplars: []metricdata.Exemplar[float64]{exemplarFloat64C}, + } + gaugeInt64A = metricdata.Gauge[int64]{ DataPoints: []metricdata.DataPoint[int64]{dataPointInt64A}, } @@ -280,6 +377,31 @@ var ( DataPoints: []metricdata.HistogramDataPoint[float64]{histogramDataPointFloat64C}, } + exponentialHistogramInt64A = metricdata.ExponentialHistogram[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.ExponentialHistogramDataPoint[int64]{exponentialHistogramDataPointInt64A}, + } + exponentialHistogramFloat64A = metricdata.ExponentialHistogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{exponentialHistogramDataPointFloat64A}, + } + exponentialHistogramInt64B = metricdata.ExponentialHistogram[int64]{ + Temporality: metricdata.DeltaTemporality, + DataPoints: []metricdata.ExponentialHistogramDataPoint[int64]{exponentialHistogramDataPointInt64B}, + } + exponentialHistogramFloat64B = metricdata.ExponentialHistogram[float64]{ + Temporality: metricdata.DeltaTemporality, + DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{exponentialHistogramDataPointFloat64B}, + } + exponentialHistogramInt64C = metricdata.ExponentialHistogram[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.ExponentialHistogramDataPoint[int64]{exponentialHistogramDataPointInt64C}, + } + exponentialHistogramFloat64C = metricdata.ExponentialHistogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{exponentialHistogramDataPointFloat64C}, + } + metricsA = metricdata.Metrics{ Name: "A", Description: "A desc", @@ -378,6 +500,11 @@ func TestAssertEqual(t *testing.T) { t.Run("ExtremaFloat64", testDatatype(minFloat64A, minFloat64B, equalExtrema[float64])) t.Run("ExemplarInt64", testDatatype(exemplarInt64A, exemplarInt64B, equalExemplars[int64])) t.Run("ExemplarFloat64", testDatatype(exemplarFloat64A, exemplarFloat64B, equalExemplars[float64])) + t.Run("ExponentialHistogramInt64", testDatatype(exponentialHistogramInt64A, exponentialHistogramInt64B, equalExponentialHistograms[int64])) + t.Run("ExponentialHistogramFloat64", testDatatype(exponentialHistogramFloat64A, exponentialHistogramFloat64B, equalExponentialHistograms[float64])) + t.Run("ExponentialHistogramDataPointInt64", testDatatype(exponentialHistogramDataPointInt64A, exponentialHistogramDataPointInt64B, equalExponentialHistogramDataPoints[int64])) + t.Run("ExponentialHistogramDataPointFloat64", testDatatype(exponentialHistogramDataPointFloat64A, exponentialHistogramDataPointFloat64B, equalExponentialHistogramDataPoints[float64])) + t.Run("ExponentialBuckets", testDatatype(exponentialBucket2, exponentialBucket3, equalExponentialBuckets)) } func TestAssertEqualIgnoreTime(t *testing.T) { @@ -398,6 +525,10 @@ func TestAssertEqualIgnoreTime(t *testing.T) { t.Run("ExtremaFloat64", testDatatypeIgnoreTime(minFloat64A, minFloat64C, equalExtrema[float64])) t.Run("ExemplarInt64", testDatatypeIgnoreTime(exemplarInt64A, exemplarInt64C, equalExemplars[int64])) t.Run("ExemplarFloat64", testDatatypeIgnoreTime(exemplarFloat64A, exemplarFloat64C, equalExemplars[float64])) + t.Run("ExponentialHistogramInt64", testDatatypeIgnoreTime(exponentialHistogramInt64A, exponentialHistogramInt64C, equalExponentialHistograms[int64])) + t.Run("ExponentialHistogramFloat64", testDatatypeIgnoreTime(exponentialHistogramFloat64A, exponentialHistogramFloat64C, equalExponentialHistograms[float64])) + t.Run("ExponentialHistogramDataPointInt64", testDatatypeIgnoreTime(exponentialHistogramDataPointInt64A, exponentialHistogramDataPointInt64C, equalExponentialHistogramDataPoints[int64])) + t.Run("ExponentialHistogramDataPointFloat64", testDatatypeIgnoreTime(exponentialHistogramDataPointFloat64A, exponentialHistogramDataPointFloat64C, equalExponentialHistogramDataPoints[float64])) } func TestAssertEqualIgnoreExemplars(t *testing.T) { @@ -416,6 +547,14 @@ func TestAssertEqualIgnoreExemplars(t *testing.T) { dpFloat64 := dataPointFloat64A dpFloat64.Exemplars = []metricdata.Exemplar[float64]{exemplarFloat64B} t.Run("DataPointFloat64", testDatatypeIgnoreExemplars(dataPointFloat64A, dpFloat64, equalDataPoints[float64])) + + ehdpInt64 := exponentialHistogramDataPointInt64A + ehdpInt64.Exemplars = []metricdata.Exemplar[int64]{exemplarInt64B} + t.Run("ExponentialHistogramDataPointInt64", testDatatypeIgnoreExemplars(exponentialHistogramDataPointInt64A, ehdpInt64, equalExponentialHistogramDataPoints[int64])) + + ehdpFloat64 := exponentialHistogramDataPointFloat64A + ehdpFloat64.Exemplars = []metricdata.Exemplar[float64]{exemplarFloat64B} + t.Run("ExponentialHistogramDataPointFloat64", testDatatypeIgnoreExemplars(exponentialHistogramDataPointFloat64A, ehdpFloat64, equalExponentialHistogramDataPoints[float64])) } type unknownAggregation struct { @@ -430,6 +569,8 @@ func TestAssertAggregationsEqual(t *testing.T) { AssertAggregationsEqual(t, gaugeFloat64A, gaugeFloat64A) AssertAggregationsEqual(t, histogramInt64A, histogramInt64A) AssertAggregationsEqual(t, histogramFloat64A, histogramFloat64A) + AssertAggregationsEqual(t, exponentialHistogramInt64A, exponentialHistogramInt64A) + AssertAggregationsEqual(t, exponentialHistogramFloat64A, exponentialHistogramFloat64A) r := equalAggregations(sumInt64A, nil, config{}) assert.Len(t, r, 1, "should return nil comparison mismatch only") @@ -475,6 +616,18 @@ func TestAssertAggregationsEqual(t *testing.T) { r = equalAggregations(histogramFloat64A, histogramFloat64C, config{ignoreTimestamp: true}) assert.Len(t, r, 0, "histograms should be equal: %v", r) + + r = equalAggregations(exponentialHistogramInt64A, exponentialHistogramInt64B, config{}) + assert.Greaterf(t, len(r), 0, "exponential histograms should not be equal: %v == %v", exponentialHistogramInt64A, exponentialHistogramInt64B) + + r = equalAggregations(exponentialHistogramInt64A, exponentialHistogramInt64C, config{ignoreTimestamp: true}) + assert.Len(t, r, 0, "exponential histograms should be equal: %v", r) + + r = equalAggregations(exponentialHistogramFloat64A, exponentialHistogramFloat64B, config{}) + assert.Greaterf(t, len(r), 0, "exponential histograms should not be equal: %v == %v", exponentialHistogramFloat64A, exponentialHistogramFloat64B) + + r = equalAggregations(exponentialHistogramFloat64A, exponentialHistogramFloat64C, config{ignoreTimestamp: true}) + assert.Len(t, r, 0, "exponential histograms should be equal: %v", r) } func TestAssertAttributes(t *testing.T) { @@ -494,6 +647,11 @@ func TestAssertAttributes(t *testing.T) { AssertHasAttributes(t, metricsA, attribute.Bool("A", true)) AssertHasAttributes(t, scopeMetricsA, attribute.Bool("A", true)) AssertHasAttributes(t, resourceMetricsA, attribute.Bool("A", true)) + AssertHasAttributes(t, exponentialHistogramDataPointInt64A, attribute.Bool("A", true)) + AssertHasAttributes(t, exponentialHistogramDataPointFloat64A, attribute.Bool("A", true)) + AssertHasAttributes(t, exponentialHistogramInt64A, attribute.Bool("A", true)) + AssertHasAttributes(t, exponentialHistogramFloat64A, attribute.Bool("A", true)) + AssertHasAttributes(t, exponentialBucket2, attribute.Bool("A", true)) // No-op, always pass. r := hasAttributesAggregation(gaugeInt64A, attribute.Bool("A", true)) assert.Equal(t, len(r), 0, "gaugeInt64A has A=True") @@ -507,6 +665,10 @@ func TestAssertAttributes(t *testing.T) { assert.Equal(t, len(r), 0, "histogramInt64A has A=True") r = hasAttributesAggregation(histogramFloat64A, attribute.Bool("A", true)) assert.Equal(t, len(r), 0, "histogramFloat64A has A=True") + r = hasAttributesAggregation(exponentialHistogramInt64A, attribute.Bool("A", true)) + assert.Equal(t, len(r), 0, "exponentialHistogramInt64A has A=True") + r = hasAttributesAggregation(exponentialHistogramFloat64A, attribute.Bool("A", true)) + assert.Equal(t, len(r), 0, "exponentialHistogramFloat64A has A=True") r = hasAttributesAggregation(gaugeInt64A, attribute.Bool("A", false)) assert.Greater(t, len(r), 0, "gaugeInt64A does not have A=False") @@ -520,6 +682,10 @@ func TestAssertAttributes(t *testing.T) { assert.Greater(t, len(r), 0, "histogramInt64A does not have A=False") r = hasAttributesAggregation(histogramFloat64A, attribute.Bool("A", false)) assert.Greater(t, len(r), 0, "histogramFloat64A does not have A=False") + r = hasAttributesAggregation(exponentialHistogramInt64A, attribute.Bool("A", false)) + assert.Greater(t, len(r), 0, "exponentialHistogramInt64A does not have A=False") + r = hasAttributesAggregation(exponentialHistogramFloat64A, attribute.Bool("A", false)) + assert.Greater(t, len(r), 0, "exponentialHistogramFloat64A does not have A=False") r = hasAttributesAggregation(gaugeInt64A, attribute.Bool("B", true)) assert.Greater(t, len(r), 0, "gaugeInt64A does not have Attribute B") @@ -533,6 +699,10 @@ func TestAssertAttributes(t *testing.T) { assert.Greater(t, len(r), 0, "histogramIntA does not have Attribute B") r = hasAttributesAggregation(histogramFloat64A, attribute.Bool("B", true)) assert.Greater(t, len(r), 0, "histogramFloatA does not have Attribute B") + r = hasAttributesAggregation(exponentialHistogramInt64A, attribute.Bool("B", true)) + assert.Greater(t, len(r), 0, "exponentialHistogramIntA does not have Attribute B") + r = hasAttributesAggregation(exponentialHistogramFloat64A, attribute.Bool("B", true)) + assert.Greater(t, len(r), 0, "exponentialHistogramFloatA does not have Attribute B") } func TestAssertAttributesFail(t *testing.T) { @@ -553,6 +723,10 @@ func TestAssertAttributesFail(t *testing.T) { assert.False(t, AssertHasAttributes(fakeT, metricsA, attribute.Bool("B", true))) assert.False(t, AssertHasAttributes(fakeT, resourceMetricsA, attribute.Bool("A", false))) assert.False(t, AssertHasAttributes(fakeT, resourceMetricsA, attribute.Bool("B", true))) + assert.False(t, AssertHasAttributes(fakeT, exponentialHistogramDataPointInt64A, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, exponentialHistogramDataPointFloat64A, attribute.Bool("B", true))) + assert.False(t, AssertHasAttributes(fakeT, exponentialHistogramInt64A, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, exponentialHistogramFloat64A, attribute.Bool("B", true))) sum := metricdata.Sum[int64]{ Temporality: metricdata.CumulativeTemporality, diff --git a/sdk/metric/metricdata/metricdatatest/comparisons.go b/sdk/metric/metricdata/metricdatatest/comparisons.go index b3ec710e9..4bb3b19fb 100644 --- a/sdk/metric/metricdata/metricdatatest/comparisons.go +++ b/sdk/metric/metricdata/metricdatatest/comparisons.go @@ -143,6 +143,18 @@ func equalAggregations(a, b metricdata.Aggregation, cfg config) (reasons []strin reasons = append(reasons, "Histogram not equal:") reasons = append(reasons, r...) } + case metricdata.ExponentialHistogram[int64]: + r := equalExponentialHistograms(v, b.(metricdata.ExponentialHistogram[int64]), cfg) + if len(r) > 0 { + reasons = append(reasons, "ExponentialHistogram not equal:") + reasons = append(reasons, r...) + } + case metricdata.ExponentialHistogram[float64]: + r := equalExponentialHistograms(v, b.(metricdata.ExponentialHistogram[float64]), cfg) + if len(r) > 0 { + reasons = append(reasons, "ExponentialHistogram not equal:") + reasons = append(reasons, r...) + } default: reasons = append(reasons, fmt.Sprintf("Aggregation of unknown types %T", a)) } @@ -312,6 +324,103 @@ func equalHistogramDataPoints[N int64 | float64](a, b metricdata.HistogramDataPo return reasons } +// equalExponentialHistograms returns reasons exponential Histograms are not equal. If they are +// equal, the returned reasons will be empty. +// +// The DataPoints each Histogram contains are compared based on containing the +// same HistogramDataPoint, not the order they are stored in. +func equalExponentialHistograms[N int64 | float64](a, b metricdata.ExponentialHistogram[N], cfg config) (reasons []string) { + if a.Temporality != b.Temporality { + reasons = append(reasons, notEqualStr("Temporality", a.Temporality, b.Temporality)) + } + + r := compareDiff(diffSlices( + a.DataPoints, + b.DataPoints, + func(a, b metricdata.ExponentialHistogramDataPoint[N]) bool { + r := equalExponentialHistogramDataPoints(a, b, cfg) + return len(r) == 0 + }, + )) + if r != "" { + reasons = append(reasons, fmt.Sprintf("Histogram DataPoints not equal:\n%s", r)) + } + return reasons +} + +// equalExponentialHistogramDataPoints returns reasons HistogramDataPoints are not equal. +// If they are equal, the returned reasons will be empty. +func equalExponentialHistogramDataPoints[N int64 | float64](a, b metricdata.ExponentialHistogramDataPoint[N], cfg config) (reasons []string) { // nolint: revive // Intentional internal control flag + if !a.Attributes.Equals(&b.Attributes) { + reasons = append(reasons, notEqualStr( + "Attributes", + a.Attributes.Encoded(attribute.DefaultEncoder()), + b.Attributes.Encoded(attribute.DefaultEncoder()), + )) + } + if !cfg.ignoreTimestamp { + if !a.StartTime.Equal(b.StartTime) { + reasons = append(reasons, notEqualStr("StartTime", a.StartTime.UnixNano(), b.StartTime.UnixNano())) + } + if !a.Time.Equal(b.Time) { + reasons = append(reasons, notEqualStr("Time", a.Time.UnixNano(), b.Time.UnixNano())) + } + } + if a.Count != b.Count { + reasons = append(reasons, notEqualStr("Count", a.Count, b.Count)) + } + if !eqExtrema(a.Min, b.Min) { + reasons = append(reasons, notEqualStr("Min", a.Min, b.Min)) + } + if !eqExtrema(a.Max, b.Max) { + reasons = append(reasons, notEqualStr("Max", a.Max, b.Max)) + } + if a.Sum != b.Sum { + reasons = append(reasons, notEqualStr("Sum", a.Sum, b.Sum)) + } + + if a.Scale != b.Scale { + reasons = append(reasons, notEqualStr("Scale", a.Scale, b.Scale)) + } + if a.ZeroCount != b.ZeroCount { + reasons = append(reasons, notEqualStr("ZeroCount", a.ZeroCount, b.ZeroCount)) + } + + r := equalExponentialBuckets(a.PositiveBucket, b.PositiveBucket, cfg) + if len(r) > 0 { + reasons = append(reasons, r...) + } + r = equalExponentialBuckets(a.NegativeBucket, b.NegativeBucket, cfg) + if len(r) > 0 { + reasons = append(reasons, r...) + } + + if !cfg.ignoreExemplars { + r := compareDiff(diffSlices( + a.Exemplars, + b.Exemplars, + func(a, b metricdata.Exemplar[N]) bool { + r := equalExemplars(a, b, cfg) + return len(r) == 0 + }, + )) + if r != "" { + reasons = append(reasons, fmt.Sprintf("Exemplars not equal:\n%s", r)) + } + } + return reasons +} + +func equalExponentialBuckets(a, b metricdata.ExponentialBucket, _ config) (reasons []string) { + if a.Offset != b.Offset { + reasons = append(reasons, notEqualStr("Offset", a.Offset, b.Offset)) + } + if !equalSlices(a.Counts, b.Counts) { + reasons = append(reasons, notEqualStr("Counts", a.Counts, b.Counts)) + } + return reasons +} + func notEqualStr(prefix string, expected, actual interface{}) string { return fmt.Sprintf("%s not equal:\nexpected: %v\nactual: %v", prefix, expected, actual) } @@ -557,6 +666,31 @@ func hasAttributesHistogram[T int64 | float64](histogram metricdata.Histogram[T] return reasons } +func hasAttributesExponentialHistogramDataPoints[T int64 | float64](dp metricdata.ExponentialHistogramDataPoint[T], attrs ...attribute.KeyValue) (reasons []string) { + for _, attr := range attrs { + val, ok := dp.Attributes.Value(attr.Key) + if !ok { + reasons = append(reasons, missingAttrStr(string(attr.Key))) + continue + } + if val != attr.Value { + reasons = append(reasons, notEqualStr(string(attr.Key), attr.Value.Emit(), val.Emit())) + } + } + return reasons +} + +func hasAttributesExponentialHistogram[T int64 | float64](histogram metricdata.ExponentialHistogram[T], attrs ...attribute.KeyValue) (reasons []string) { + for n, dp := range histogram.DataPoints { + reas := hasAttributesExponentialHistogramDataPoints(dp, attrs...) + if len(reas) > 0 { + reasons = append(reasons, fmt.Sprintf("histogram datapoint %d attributes:\n", n)) + reasons = append(reasons, reas...) + } + } + return reasons +} + func hasAttributesAggregation(agg metricdata.Aggregation, attrs ...attribute.KeyValue) (reasons []string) { switch agg := agg.(type) { case metricdata.Gauge[int64]: @@ -571,6 +705,10 @@ func hasAttributesAggregation(agg metricdata.Aggregation, attrs ...attribute.Key reasons = hasAttributesHistogram(agg, attrs...) case metricdata.Histogram[float64]: reasons = hasAttributesHistogram(agg, attrs...) + case metricdata.ExponentialHistogram[int64]: + reasons = hasAttributesExponentialHistogram(agg, attrs...) + case metricdata.ExponentialHistogram[float64]: + reasons = hasAttributesExponentialHistogram(agg, attrs...) default: reasons = []string{fmt.Sprintf("unknown aggregation %T", agg)} }