diff --git a/CHANGELOG.md b/CHANGELOG.md index 5261ba19d95..c3c8e07b02c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add Summary, SummaryDataPoint, and QuantileValue to `go.opentelemetry.io/sdk/metric/metricdata`. (#4622) - `go.opentelemetry.io/otel/bridge/opencensus.NewMetricProducer` now supports exemplars from OpenCensus. (#4585) - Add support for `WithExplicitBucketBoundaries` in `go.opentelemetry.io/otel/sdk/metric`. (#4605) +- Add support for Summary metrics in `go.opentelemetry.io/otel/bridge/opencensus`. (#4668) ### Deprecated diff --git a/bridge/opencensus/doc.go b/bridge/opencensus/doc.go index df62b1c575e..70990920474 100644 --- a/bridge/opencensus/doc.go +++ b/bridge/opencensus/doc.go @@ -56,7 +56,6 @@ // implemented, and An error will be sent to the OpenTelemetry ErrorHandler. // // There are known limitations to the metric bridge: -// - Summary-typed metrics are dropped // - GaugeDistribution-typed metrics are dropped // - Histogram's SumOfSquaredDeviation field is dropped package opencensus // import "go.opentelemetry.io/otel/bridge/opencensus" diff --git a/bridge/opencensus/internal/ocmetric/metric.go b/bridge/opencensus/internal/ocmetric/metric.go index aeb3479f40b..8fdd0eb6167 100644 --- a/bridge/opencensus/internal/ocmetric/metric.go +++ b/bridge/opencensus/internal/ocmetric/metric.go @@ -32,7 +32,7 @@ import ( var ( errAggregationType = errors.New("unsupported OpenCensus aggregation type") errMismatchedValueTypes = errors.New("wrong value type for data point") - errNegativeDistributionCount = errors.New("distribution count is negative") + errNegativeCount = errors.New("distribution or summary count is negative") errNegativeBucketCount = errors.New("distribution bucket count is negative") errMismatchedAttributeKeyValues = errors.New("mismatched number of attribute keys and values") errInvalidExemplarSpanContext = errors.New("span context exemplar attachment does not contain an OpenCensus SpanContext") @@ -78,7 +78,8 @@ func convertAggregation(metric *ocmetricdata.Metric) (metricdata.Aggregation, er return convertSum[float64](labelKeys, metric.TimeSeries) case ocmetricdata.TypeCumulativeDistribution: return convertHistogram(labelKeys, metric.TimeSeries) - // TODO: Support summaries, once it is in the OTel data types. + case ocmetricdata.TypeSummary: + return convertSummary(labelKeys, metric.TimeSeries) } return nil, fmt.Errorf("%w: %q", errAggregationType, metric.Descriptor.Type) } @@ -146,7 +147,7 @@ func convertHistogram(labelKeys []ocmetricdata.LabelKey, ts []*ocmetricdata.Time continue } if dist.Count < 0 { - err = errors.Join(err, fmt.Errorf("%w: %d", errNegativeDistributionCount, dist.Count)) + err = errors.Join(err, fmt.Errorf("%w: %d", errNegativeCount, dist.Count)) continue } points = append(points, metricdata.HistogramDataPoint[float64]{ @@ -340,6 +341,65 @@ func complexToString[N complex64 | complex128](val N) string { return strconv.FormatComplex(complex128(val), 'f', -1, 64) } +// convertSummary converts OpenCensus Summary timeseries to an +// OpenTelemetry Summary. +func convertSummary(labelKeys []ocmetricdata.LabelKey, ts []*ocmetricdata.TimeSeries) (metricdata.Summary, error) { + points := make([]metricdata.SummaryDataPoint, 0, len(ts)) + var err error + for _, t := range ts { + attrs, attrErr := convertAttrs(labelKeys, t.LabelValues) + if attrErr != nil { + err = errors.Join(err, attrErr) + continue + } + for _, p := range t.Points { + summary, ok := p.Value.(*ocmetricdata.Summary) + if !ok { + err = errors.Join(err, fmt.Errorf("%w: %d", errMismatchedValueTypes, p.Value)) + continue + } + if summary.Count < 0 { + err = errors.Join(err, fmt.Errorf("%w: %d", errNegativeCount, summary.Count)) + continue + } + point := metricdata.SummaryDataPoint{ + Attributes: attrs, + StartTime: t.StartTime, + Time: p.Time, + Count: uint64(summary.Count), + QuantileValues: convertQuantiles(summary.Snapshot), + Sum: summary.Sum, + } + points = append(points, point) + } + } + return metricdata.Summary{DataPoints: points}, err +} + +// convertQuantiles converts an OpenCensus summary snapshot to +// OpenTelemetry quantiles. +func convertQuantiles(snapshot ocmetricdata.Snapshot) []metricdata.QuantileValue { + quantileValues := make([]metricdata.QuantileValue, 0, len(snapshot.Percentiles)) + for quantile, value := range snapshot.Percentiles { + quantileValues = append(quantileValues, metricdata.QuantileValue{ + // OpenCensus quantiles are range (0-100.0], but OpenTelemetry + // quantiles are range [0.0, 1.0]. + Quantile: quantile / 100.0, + Value: value, + }) + } + sort.Sort(byQuantile(quantileValues)) + return quantileValues +} + +// byQuantile implements sort.Interface for []metricdata.QuantileValue +// based on the Quantile field. +type byQuantile []metricdata.QuantileValue + +func (a byQuantile) Len() int { return len(a) } +func (a byQuantile) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byQuantile) Less(i, j int) bool { return a[i].Quantile < a[j].Quantile } + // convertAttrs converts from OpenCensus attribute keys and values to an // OpenTelemetry attribute Set. func convertAttrs(keys []ocmetricdata.LabelKey, values []ocmetricdata.LabelValue) (attribute.Set, error) { diff --git a/bridge/opencensus/internal/ocmetric/metric_test.go b/bridge/opencensus/internal/ocmetric/metric_test.go index bf851bc14ad..b7a25e5c776 100644 --- a/bridge/opencensus/internal/ocmetric/metric_test.go +++ b/bridge/opencensus/internal/ocmetric/metric_test.go @@ -47,7 +47,7 @@ func TestConvertMetrics(t *testing.T) { expected: []metricdata.Metrics{}, }, { - desc: "normal Histogram, gauges, and sums", + desc: "normal Histogram, summary, gauges, and sums", input: []*ocmetricdata.Metric{ { Descriptor: ocmetricdata.Descriptor{ @@ -285,6 +285,54 @@ func TestConvertMetrics(t *testing.T) { }, }, }, + }, { + Descriptor: ocmetricdata.Descriptor{ + Name: "foo.com/summary-a", + Description: "a testing summary", + Unit: ocmetricdata.UnitMilliseconds, + Type: ocmetricdata.TypeSummary, + LabelKeys: []ocmetricdata.LabelKey{ + {Key: "g"}, + {Key: "h"}, + }, + }, + TimeSeries: []*ocmetricdata.TimeSeries{ + { + LabelValues: []ocmetricdata.LabelValue{ + { + Value: "ding", + Present: true, + }, { + Value: "dong", + Present: true, + }, + }, + Points: []ocmetricdata.Point{ + ocmetricdata.NewSummaryPoint(endTime1, &ocmetricdata.Summary{ + Count: 10, + Sum: 13.2, + HasCountAndSum: true, + Snapshot: ocmetricdata.Snapshot{ + Percentiles: map[float64]float64{ + 50.0: 1.0, + 0.0: 0.1, + 100.0: 10.4, + }, + }, + }), + ocmetricdata.NewSummaryPoint(endTime2, &ocmetricdata.Summary{ + Count: 12, + Snapshot: ocmetricdata.Snapshot{ + Percentiles: map[float64]float64{ + 0.0: 0.2, + 50.0: 1.1, + 100.0: 10.5, + }, + }, + }), + }, + }, + }, }, }, expected: []metricdata.Metrics{ @@ -489,6 +537,64 @@ func TestConvertMetrics(t *testing.T) { }, }, }, + }, { + Name: "foo.com/summary-a", + Description: "a testing summary", + Unit: "ms", + Data: metricdata.Summary{ + DataPoints: []metricdata.SummaryDataPoint{ + { + Attributes: attribute.NewSet(attribute.KeyValue{ + Key: attribute.Key("g"), + Value: attribute.StringValue("ding"), + }, attribute.KeyValue{ + Key: attribute.Key("h"), + Value: attribute.StringValue("dong"), + }), + Time: endTime1, + Count: 10, + Sum: 13.2, + QuantileValues: []metricdata.QuantileValue{ + { + Quantile: 0.0, + Value: 0.1, + }, + { + Quantile: 0.5, + Value: 1.0, + }, + { + Quantile: 1.0, + Value: 10.4, + }, + }, + }, { + Attributes: attribute.NewSet(attribute.KeyValue{ + Key: attribute.Key("g"), + Value: attribute.StringValue("ding"), + }, attribute.KeyValue{ + Key: attribute.Key("h"), + Value: attribute.StringValue("dong"), + }), + Time: endTime2, + Count: 12, + QuantileValues: []metricdata.QuantileValue{ + { + Quantile: 0.0, + Value: 0.2, + }, + { + Quantile: 0.5, + Value: 1.1, + }, + { + Quantile: 1.0, + Value: 10.5, + }, + }, + }, + }, + }, }, }, }, @@ -586,7 +692,7 @@ func TestConvertMetrics(t *testing.T) { }, }, }, - expectedErr: errNegativeDistributionCount, + expectedErr: errNegativeCount, }, { desc: "histogram with negative bucket count", @@ -638,6 +744,82 @@ func TestConvertMetrics(t *testing.T) { }, expectedErr: errMismatchedValueTypes, }, + { + desc: "summary with mismatched attributes", + input: []*ocmetricdata.Metric{ + { + Descriptor: ocmetricdata.Descriptor{ + Name: "foo.com/summary-mismatched", + Description: "a mismatched summary", + Unit: ocmetricdata.UnitMilliseconds, + Type: ocmetricdata.TypeSummary, + LabelKeys: []ocmetricdata.LabelKey{ + {Key: "g"}, + }, + }, + TimeSeries: []*ocmetricdata.TimeSeries{ + { + LabelValues: []ocmetricdata.LabelValue{ + { + Value: "ding", + Present: true, + }, { + Value: "dong", + Present: true, + }, + }, + Points: []ocmetricdata.Point{ + ocmetricdata.NewSummaryPoint(endTime1, &ocmetricdata.Summary{ + Count: 10, + Sum: 13.2, + HasCountAndSum: true, + Snapshot: ocmetricdata.Snapshot{ + Percentiles: map[float64]float64{ + 0.0: 0.1, + 0.5: 1.0, + 1.0: 10.4, + }, + }, + }), + }, + }, + }, + }, + }, + expectedErr: errMismatchedAttributeKeyValues, + }, + { + desc: "summary with negative count", + input: []*ocmetricdata.Metric{ + { + Descriptor: ocmetricdata.Descriptor{ + Name: "foo.com/summary-negative", + Description: "a negative count summary", + Unit: ocmetricdata.UnitMilliseconds, + Type: ocmetricdata.TypeSummary, + }, + TimeSeries: []*ocmetricdata.TimeSeries{ + { + Points: []ocmetricdata.Point{ + ocmetricdata.NewSummaryPoint(endTime1, &ocmetricdata.Summary{ + Count: -10, + Sum: 13.2, + HasCountAndSum: true, + Snapshot: ocmetricdata.Snapshot{ + Percentiles: map[float64]float64{ + 0.0: 0.1, + 0.5: 1.0, + 1.0: 10.4, + }, + }, + }), + }, + }, + }, + }, + }, + expectedErr: errNegativeCount, + }, { desc: "histogram with invalid span context exemplar", input: []*ocmetricdata.Metric{ @@ -722,6 +904,28 @@ func TestConvertMetrics(t *testing.T) { }, expectedErr: errMismatchedValueTypes, }, + { + desc: "summary with non-summary datapoint type", + input: []*ocmetricdata.Metric{ + { + Descriptor: ocmetricdata.Descriptor{ + Name: "foo.com/bad-point", + Description: "a bad type", + Unit: ocmetricdata.UnitDimensionless, + Type: ocmetricdata.TypeSummary, + }, + TimeSeries: []*ocmetricdata.TimeSeries{ + { + Points: []ocmetricdata.Point{ + ocmetricdata.NewDistributionPoint(endTime1, &ocmetricdata.Distribution{}), + }, + StartTime: startTime, + }, + }, + }, + }, + expectedErr: errMismatchedValueTypes, + }, { desc: "unsupported Gauge Distribution type", input: []*ocmetricdata.Metric{ @@ -740,7 +944,7 @@ func TestConvertMetrics(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { output, err := ConvertMetrics(tc.input) if !errors.Is(err, tc.expectedErr) { - t.Errorf("convertAggregation(%+v) = err(%v), want err(%v)", tc.input, err, tc.expectedErr) + t.Errorf("ConvertMetrics(%+v) = err(%v), want err(%v)", tc.input, err, tc.expectedErr) } metricdatatest.AssertEqual[metricdata.ScopeMetrics](t, metricdata.ScopeMetrics{Metrics: tc.expected}, diff --git a/sdk/metric/metricdata/metricdatatest/comparisons.go b/sdk/metric/metricdata/metricdatatest/comparisons.go index c6407aca8f8..a25c930676f 100644 --- a/sdk/metric/metricdata/metricdatatest/comparisons.go +++ b/sdk/metric/metricdata/metricdatatest/comparisons.go @@ -472,7 +472,7 @@ func equalSummaryDataPoint(a, b metricdata.SummaryDataPoint, cfg config) (reason } r := compareDiff(diffSlices( a.QuantileValues, - a.QuantileValues, + b.QuantileValues, func(a, b metricdata.QuantileValue) bool { r := equalQuantileValue(a, b, cfg) return len(r) == 0