diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b129dbd6fc..162bf639470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ See our [versioning policy](VERSIONING.md) for more information about these stab - 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) +- OTLP metrics exporter now supports the Exponential Histogram Data Type. (#4222) ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2099575bd76..72c1b9eb13c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -475,8 +475,33 @@ documentation are allowed to be extended with additional methods. > Warning: methods may be added to this interface in minor releases. +These interfaces are defined by the OpenTelemetry specification and will be +updated as the specification evolves. + Otherwise, stable interfaces MUST NOT be modified. +#### How to Change Specification Interfaces + +When an API change must be made, we will update the SDK with the new method one +release before the API change. This will allow the SDK one version before the +API change to work seamlessly with the new API. + +If an incompatible version of the SDK is used with the new API the application +will fail to compile. + +#### How Not to Change Specification Interfaces + +We have explored using a v2 of the API to change interfaces and found that there +was no way to introduce a v2 and have it work seamlessly with the v1 of the API. +Problems happened with libraries that upgraded to v2 when an application did not, +and would not produce any telemetry. + +More detail of the approaches considered and their limitations can be found in +the [Use a V2 API to evolve interfaces](https://github.com/open-telemetry/opentelemetry-go/issues/3920) +issue. + +#### How to Change Other Interfaces + If new functionality is needed for an interface that cannot be changed it MUST be added by including an additional interface. That added interface can be a simple interface for the specific functionality that you want to add or it can diff --git a/exporters/otlp/otlpmetric/internal/transform/metricdata.go b/exporters/otlp/otlpmetric/internal/transform/metricdata.go index 2f98115b83d..809ddbdf570 100644 --- a/exporters/otlp/otlpmetric/internal/transform/metricdata.go +++ b/exporters/otlp/otlpmetric/internal/transform/metricdata.go @@ -99,6 +99,10 @@ func metric(m metricdata.Metrics) (*mpb.Metric, error) { out.Data, err = Histogram(a) case metricdata.Histogram[float64]: out.Data, err = Histogram(a) + case metricdata.ExponentialHistogram[int64]: + out.Data, err = ExponentialHistogram(a) + case metricdata.ExponentialHistogram[float64]: + out.Data, err = ExponentialHistogram(a) default: return out, fmt.Errorf("%w: %T", errUnknownAggregation, a) } @@ -114,8 +118,8 @@ func Gauge[N int64 | float64](g metricdata.Gauge[N]) *mpb.Metric_Gauge { } } -// Sum returns an OTLP Metric_Sum generated from s. An error is returned with -// a partial Metric_Sum if the temporality of s is unknown. +// Sum returns an OTLP Metric_Sum generated from s. An error is returned +// if the temporality of s is unknown. func Sum[N int64 | float64](s metricdata.Sum[N]) (*mpb.Metric_Sum, error) { t, err := Temporality(s.Temporality) if err != nil { @@ -155,8 +159,7 @@ func DataPoints[N int64 | float64](dPts []metricdata.DataPoint[N]) []*mpb.Number } // Histogram returns an OTLP Metric_Histogram generated from h. An error is -// returned with a partial Metric_Histogram if the temporality of h is -// unknown. +// returned if the temporality of h is unknown. func Histogram[N int64 | float64](h metricdata.Histogram[N]) (*mpb.Metric_Histogram, error) { t, err := Temporality(h.Temporality) if err != nil { @@ -198,6 +201,61 @@ func HistogramDataPoints[N int64 | float64](dPts []metricdata.HistogramDataPoint return out } +// ExponentialHistogram returns an OTLP Metric_ExponentialHistogram generated from h. An error is +// returned if the temporality of h is unknown. +func ExponentialHistogram[N int64 | float64](h metricdata.ExponentialHistogram[N]) (*mpb.Metric_ExponentialHistogram, error) { + t, err := Temporality(h.Temporality) + if err != nil { + return nil, err + } + return &mpb.Metric_ExponentialHistogram{ + ExponentialHistogram: &mpb.ExponentialHistogram{ + AggregationTemporality: t, + DataPoints: ExponentialHistogramDataPoints(h.DataPoints), + }, + }, nil +} + +// ExponentialHistogramDataPoints returns a slice of OTLP ExponentialHistogramDataPoint generated +// from dPts. +func ExponentialHistogramDataPoints[N int64 | float64](dPts []metricdata.ExponentialHistogramDataPoint[N]) []*mpb.ExponentialHistogramDataPoint { + out := make([]*mpb.ExponentialHistogramDataPoint, 0, len(dPts)) + for _, dPt := range dPts { + sum := float64(dPt.Sum) + ehdp := &mpb.ExponentialHistogramDataPoint{ + Attributes: AttrIter(dPt.Attributes.Iter()), + StartTimeUnixNano: uint64(dPt.StartTime.UnixNano()), + TimeUnixNano: uint64(dPt.Time.UnixNano()), + Count: dPt.Count, + Sum: &sum, + Scale: dPt.Scale, + ZeroCount: dPt.ZeroCount, + + Positive: ExponentialHistogramDataPointBuckets(dPt.PositiveBucket), + Negative: ExponentialHistogramDataPointBuckets(dPt.NegativeBucket), + } + if v, ok := dPt.Min.Value(); ok { + vF64 := float64(v) + ehdp.Min = &vF64 + } + if v, ok := dPt.Max.Value(); ok { + vF64 := float64(v) + ehdp.Max = &vF64 + } + out = append(out, ehdp) + } + return out +} + +// ExponentialHistogramDataPointBuckets returns an OTLP ExponentialHistogramDataPoint_Buckets generated +// from bucket. +func ExponentialHistogramDataPointBuckets(bucket metricdata.ExponentialBucket) *mpb.ExponentialHistogramDataPoint_Buckets { + return &mpb.ExponentialHistogramDataPoint_Buckets{ + Offset: bucket.Offset, + BucketCounts: bucket.Counts, + } +} + // Temporality returns an OTLP AggregationTemporality generated from t. If t // is unknown, an error is returned along with the invalid // AggregationTemporality_AGGREGATION_TEMPORALITY_UNSPECIFIED. diff --git a/exporters/otlp/otlpmetric/internal/transform/metricdata_test.go b/exporters/otlp/otlpmetric/internal/transform/metricdata_test.go index d9a8ddf6a7c..d6ffa7c2c06 100644 --- a/exporters/otlp/otlpmetric/internal/transform/metricdata_test.go +++ b/exporters/otlp/otlpmetric/internal/transform/metricdata_test.go @@ -95,6 +95,78 @@ var ( Sum: sumB, }} + otelEBucketA = metricdata.ExponentialBucket{ + Offset: 5, + Counts: []uint64{0, 5, 0, 5}, + } + otelEBucketB = metricdata.ExponentialBucket{ + Offset: 3, + Counts: []uint64{0, 5, 0, 5}, + } + otelEBucketsC = metricdata.ExponentialBucket{ + Offset: 5, + Counts: []uint64{0, 1}, + } + otelEBucketsD = metricdata.ExponentialBucket{ + Offset: 3, + Counts: []uint64{0, 1}, + } + + otelEHDPInt64 = []metricdata.ExponentialHistogramDataPoint[int64]{{ + Attributes: alice, + StartTime: start, + Time: end, + Count: 30, + Scale: 2, + ZeroCount: 10, + PositiveBucket: otelEBucketA, + NegativeBucket: otelEBucketB, + ZeroThreshold: .01, + Min: metricdata.NewExtrema(int64(minA)), + Max: metricdata.NewExtrema(int64(maxA)), + Sum: int64(sumA), + }, { + Attributes: bob, + StartTime: start, + Time: end, + Count: 3, + Scale: 4, + ZeroCount: 1, + PositiveBucket: otelEBucketsC, + NegativeBucket: otelEBucketsD, + ZeroThreshold: .02, + Min: metricdata.NewExtrema(int64(minB)), + Max: metricdata.NewExtrema(int64(maxB)), + Sum: int64(sumB), + }} + otelEHDPFloat64 = []metricdata.ExponentialHistogramDataPoint[float64]{{ + Attributes: alice, + StartTime: start, + Time: end, + Count: 30, + Scale: 2, + ZeroCount: 10, + PositiveBucket: otelEBucketA, + NegativeBucket: otelEBucketB, + ZeroThreshold: .01, + Min: metricdata.NewExtrema(minA), + Max: metricdata.NewExtrema(maxA), + Sum: sumA, + }, { + Attributes: bob, + StartTime: start, + Time: end, + Count: 3, + Scale: 4, + ZeroCount: 1, + PositiveBucket: otelEBucketsC, + NegativeBucket: otelEBucketsD, + ZeroThreshold: .02, + Min: metricdata.NewExtrema(minB), + Max: metricdata.NewExtrema(maxB), + Sum: sumB, + }} + pbHDP = []*mpb.HistogramDataPoint{{ Attributes: []*cpb.KeyValue{pbAlice}, StartTimeUnixNano: uint64(start.UnixNano()), @@ -117,6 +189,49 @@ var ( Max: &maxB, }} + pbEHDPBA = &mpb.ExponentialHistogramDataPoint_Buckets{ + Offset: 5, + BucketCounts: []uint64{0, 5, 0, 5}, + } + pbEHDPBB = &mpb.ExponentialHistogramDataPoint_Buckets{ + Offset: 3, + BucketCounts: []uint64{0, 5, 0, 5}, + } + pbEHDPBC = &mpb.ExponentialHistogramDataPoint_Buckets{ + Offset: 5, + BucketCounts: []uint64{0, 1}, + } + pbEHDPBD = &mpb.ExponentialHistogramDataPoint_Buckets{ + Offset: 3, + BucketCounts: []uint64{0, 1}, + } + + pbEHDP = []*mpb.ExponentialHistogramDataPoint{{ + Attributes: []*cpb.KeyValue{pbAlice}, + StartTimeUnixNano: uint64(start.UnixNano()), + TimeUnixNano: uint64(end.UnixNano()), + Count: 30, + Sum: &sumA, + Scale: 2, + ZeroCount: 10, + Positive: pbEHDPBA, + Negative: pbEHDPBB, + Min: &minA, + Max: &maxA, + }, { + Attributes: []*cpb.KeyValue{pbBob}, + StartTimeUnixNano: uint64(start.UnixNano()), + TimeUnixNano: uint64(end.UnixNano()), + Count: 3, + Sum: &sumB, + Scale: 4, + ZeroCount: 1, + Positive: pbEHDPBC, + Negative: pbEHDPBD, + Min: &minB, + Max: &maxB, + }} + otelHistInt64 = metricdata.Histogram[int64]{ Temporality: metricdata.DeltaTemporality, DataPoints: otelHDPInt64, @@ -131,11 +246,29 @@ var ( DataPoints: otelHDPInt64, } + otelExpoHistInt64 = metricdata.ExponentialHistogram[int64]{ + Temporality: metricdata.DeltaTemporality, + DataPoints: otelEHDPInt64, + } + otelExpoHistFloat64 = metricdata.ExponentialHistogram[float64]{ + Temporality: metricdata.DeltaTemporality, + DataPoints: otelEHDPFloat64, + } + otelExpoHistInvalid = metricdata.ExponentialHistogram[int64]{ + Temporality: invalidTemporality, + DataPoints: otelEHDPInt64, + } + pbHist = &mpb.Histogram{ AggregationTemporality: mpb.AggregationTemporality_AGGREGATION_TEMPORALITY_DELTA, DataPoints: pbHDP, } + pbExpoHist = &mpb.ExponentialHistogram{ + AggregationTemporality: mpb.AggregationTemporality_AGGREGATION_TEMPORALITY_DELTA, + DataPoints: pbEHDP, + } + otelDPtsInt64 = []metricdata.DataPoint[int64]{ {Attributes: alice, StartTime: start, Time: end, Value: 1}, {Attributes: bob, StartTime: start, Time: end, Value: 2}, @@ -263,6 +396,24 @@ var ( Unit: "1", Data: unknownAgg, }, + { + Name: "int64-ExponentialHistogram", + Description: "Exponential Histogram", + Unit: "1", + Data: otelExpoHistInt64, + }, + { + Name: "float64-ExponentialHistogram", + Description: "Exponential Histogram", + Unit: "1", + Data: otelExpoHistFloat64, + }, + { + Name: "invalid-ExponentialHistogram", + Description: "Invalid Exponential Histogram", + Unit: "1", + Data: otelExpoHistInvalid, + }, } pbMetrics = []*mpb.Metric{ @@ -302,6 +453,18 @@ var ( Unit: "1", Data: &mpb.Metric_Histogram{Histogram: pbHist}, }, + { + Name: "int64-ExponentialHistogram", + Description: "Exponential Histogram", + Unit: "1", + Data: &mpb.Metric_ExponentialHistogram{ExponentialHistogram: pbExpoHist}, + }, + { + Name: "float64-ExponentialHistogram", + Description: "Exponential Histogram", + Unit: "1", + Data: &mpb.Metric_ExponentialHistogram{ExponentialHistogram: pbExpoHist}, + }, } otelScopeMetrics = []metricdata.ScopeMetrics{{ @@ -368,6 +531,9 @@ func TestTransformations(t *testing.T) { assert.Equal(t, pbHDP, HistogramDataPoints(otelHDPFloat64)) assert.Equal(t, pbDPtsInt64, DataPoints[int64](otelDPtsInt64)) require.Equal(t, pbDPtsFloat64, DataPoints[float64](otelDPtsFloat64)) + assert.Equal(t, pbEHDP, ExponentialHistogramDataPoints(otelEHDPInt64)) + assert.Equal(t, pbEHDP, ExponentialHistogramDataPoints(otelEHDPFloat64)) + assert.Equal(t, pbEHDPBA, ExponentialHistogramDataPointBuckets(otelEBucketA)) // Aggregations. h, err := Histogram(otelHistInt64) @@ -393,6 +559,16 @@ func TestTransformations(t *testing.T) { assert.Equal(t, &mpb.Metric_Gauge{Gauge: pbGaugeInt64}, Gauge[int64](otelGaugeInt64)) require.Equal(t, &mpb.Metric_Gauge{Gauge: pbGaugeFloat64}, Gauge[float64](otelGaugeFloat64)) + e, err := ExponentialHistogram(otelExpoHistInt64) + assert.NoError(t, err) + assert.Equal(t, &mpb.Metric_ExponentialHistogram{ExponentialHistogram: pbExpoHist}, e) + e, err = ExponentialHistogram(otelExpoHistFloat64) + assert.NoError(t, err) + assert.Equal(t, &mpb.Metric_ExponentialHistogram{ExponentialHistogram: pbExpoHist}, e) + e, err = ExponentialHistogram(otelExpoHistInvalid) + assert.ErrorIs(t, err, errUnknownTemporality) + assert.Nil(t, e) + // Metrics. m, err := Metrics(otelMetrics) assert.ErrorIs(t, err, errUnknownTemporality)