From 7be2ca635773bbdbd4ff75deb93a77940b913ae0 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Mon, 9 Oct 2023 20:26:35 +0000 Subject: [PATCH] support summaries in the OpenCensus bridge --- CHANGELOG.md | 1 + bridge/opencensus/doc.go | 1 - bridge/opencensus/internal/ocmetric/metric.go | 65 ++++++++++- .../internal/ocmetric/metric_test.go | 110 +++++++++++++++++- 4 files changed, 171 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3826a72584c4..fa295bb3097c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `Version` function in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`. (#4660) - Add `Version` function in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#4660) - Add Summary, SummaryDataPoint, and QuantileValue to `go.opentelemetry.io/sdk/metric/metricdata`. (#4622) +- 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 ed2a4cfd9356..7a4ed70fa178 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 // - Exemplars on Histograms are dropped diff --git a/bridge/opencensus/internal/ocmetric/metric.go b/bridge/opencensus/internal/ocmetric/metric.go index d656e5d28715..73b0f2f46a40 100644 --- a/bridge/opencensus/internal/ocmetric/metric.go +++ b/bridge/opencensus/internal/ocmetric/metric.go @@ -17,6 +17,7 @@ package internal // import "go.opentelemetry.io/otel/bridge/opencensus/internal/ import ( "errors" "fmt" + "sort" ocmetricdata "go.opencensus.io/metric/metricdata" @@ -27,7 +28,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") ) @@ -72,7 +73,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) } @@ -140,7 +142,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 } // TODO: handle exemplars @@ -170,6 +172,63 @@ func convertBucketCounts(buckets []ocmetricdata.Bucket) ([]uint64, error) { return bucketCounts, nil } +// 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{ + Quantile: quantile, + Value: value, + }) + } + sort.Sort(byQuantile(quantileValues)) + return quantileValues +} + +// ByAge 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 a3258c6b1e7e..3d8369dd2087 100644 --- a/bridge/opencensus/internal/ocmetric/metric_test.go +++ b/bridge/opencensus/internal/ocmetric/metric_test.go @@ -41,7 +41,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{ @@ -206,6 +206,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{ + 0.0: 0.1, + 0.5: 1.0, + 1.0: 10.4, + }, + }, + }), + ocmetricdata.NewSummaryPoint(endTime2, &ocmetricdata.Summary{ + Count: 12, + Snapshot: ocmetricdata.Snapshot{ + Percentiles: map[float64]float64{ + 0.0: 0.2, + 0.5: 1.1, + 1.0: 10.5, + }, + }, + }), + }, + }, + }, }, }, expected: []metricdata.Metrics{ @@ -367,6 +415,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, + }, + }, + }, + }, + }, }, }, }, @@ -464,7 +570,7 @@ func TestConvertMetrics(t *testing.T) { }, }, }, - expectedErr: errNegativeDistributionCount, + expectedErr: errNegativeCount, }, { desc: "histogram with negative bucket count",