Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exemplar support to OpenCensus bridge #4585

Merged
merged 10 commits into from
Oct 27, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add the `go.opentelemetry.io/otel/trace/noop` package as a default no-op implementation of the trace API. (#4620)
- Add context propagation in `go.opentelemetry.io/otel/example/dice`. (#4644)
- Add view configuration to `go.opentelemetry.io/otel/example/prometheus`. (#4649)
- `go.opentelemetry.io/otel/bridge/opencensus.NewMetricProducer` now supports exemplars from OpenCensus. (#4585)

### Deprecated

Expand Down
1 change: 0 additions & 1 deletion bridge/opencensus/doc.go
Expand Up @@ -59,5 +59,4 @@
// - Summary-typed metrics are dropped
// - GaugeDistribution-typed metrics are dropped
// - Histogram's SumOfSquaredDeviation field is dropped
// - Exemplars on Histograms are dropped
package opencensus // import "go.opentelemetry.io/otel/bridge/opencensus"
182 changes: 176 additions & 6 deletions bridge/opencensus/internal/ocmetric/metric.go
Expand Up @@ -17,8 +17,13 @@ package internal // import "go.opentelemetry.io/otel/bridge/opencensus/internal/
import (
"errors"
"fmt"
"math"
"reflect"
"sort"
"strconv"

ocmetricdata "go.opencensus.io/metric/metricdata"
octrace "go.opencensus.io/trace"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
Expand All @@ -30,6 +35,7 @@ var (
errNegativeDistributionCount = errors.New("distribution 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")
)

// ConvertMetrics converts metric data from OpenCensus to OpenTelemetry.
Expand Down Expand Up @@ -134,7 +140,7 @@ func convertHistogram(labelKeys []ocmetricdata.LabelKey, ts []*ocmetricdata.Time
err = errors.Join(err, fmt.Errorf("%w: %d", errMismatchedValueTypes, p.Value))
continue
}
bucketCounts, bucketErr := convertBucketCounts(dist.Buckets)
bucketCounts, exemplars, bucketErr := convertBuckets(dist.Buckets)
if bucketErr != nil {
err = errors.Join(err, bucketErr)
continue
Expand All @@ -143,7 +149,6 @@ func convertHistogram(labelKeys []ocmetricdata.LabelKey, ts []*ocmetricdata.Time
err = errors.Join(err, fmt.Errorf("%w: %d", errNegativeDistributionCount, dist.Count))
continue
}
// TODO: handle exemplars
points = append(points, metricdata.HistogramDataPoint[float64]{
Attributes: attrs,
StartTime: t.StartTime,
Expand All @@ -152,22 +157,187 @@ func convertHistogram(labelKeys []ocmetricdata.LabelKey, ts []*ocmetricdata.Time
Sum: dist.Sum,
Bounds: dist.BucketOptions.Bounds,
BucketCounts: bucketCounts,
Exemplars: exemplars,
})
}
}
return metricdata.Histogram[float64]{DataPoints: points, Temporality: metricdata.CumulativeTemporality}, err
}

// convertBucketCounts converts from OpenCensus bucket counts to slice of uint64.
func convertBucketCounts(buckets []ocmetricdata.Bucket) ([]uint64, error) {
// convertBuckets converts from OpenCensus bucket counts to slice of uint64,
// and converts OpenCensus exemplars to OpenTelemetry exemplars.
func convertBuckets(buckets []ocmetricdata.Bucket) ([]uint64, []metricdata.Exemplar[float64], error) {
bucketCounts := make([]uint64, len(buckets))
exemplars := []metricdata.Exemplar[float64]{}
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
var err error
for i, bucket := range buckets {
if bucket.Count < 0 {
return nil, fmt.Errorf("%w: %q", errNegativeBucketCount, bucket.Count)
err = errors.Join(err, fmt.Errorf("%w: %q", errNegativeBucketCount, bucket.Count))
continue
}
bucketCounts[i] = uint64(bucket.Count)

if bucket.Exemplar != nil {
exemplar, exemplarErr := convertExemplar(bucket.Exemplar)
if exemplarErr != nil {
err = errors.Join(err, exemplarErr)
continue
}
exemplars = append(exemplars, exemplar)
}
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
}
return bucketCounts, exemplars, err
}

// convertExemplar converts an OpenCensus exemplar to an OpenTelemetry exemplar.
func convertExemplar(ocExemplar *ocmetricdata.Exemplar) (metricdata.Exemplar[float64], error) {
exemplar := metricdata.Exemplar[float64]{
Value: ocExemplar.Value,
Time: ocExemplar.Timestamp,
}
var err error
for k, v := range ocExemplar.Attachments {
switch {
case k == ocmetricdata.AttachmentKeySpanContext:
sc, ok := v.(octrace.SpanContext)
if !ok {
err = errors.Join(err, fmt.Errorf("%w; type: %v", errInvalidExemplarSpanContext, reflect.TypeOf(v)))
continue
}
exemplar.SpanID = sc.SpanID[:]
exemplar.TraceID = sc.TraceID[:]
default:
exemplar.FilteredAttributes = append(exemplar.FilteredAttributes, convertKV(k, v))
}
}
sortable := attribute.Sortable(exemplar.FilteredAttributes)
sort.Sort(&sortable)
return exemplar, err
}

// convertKV converts an OpenCensus Attachment to an OpenTelemetry KeyValue.
func convertKV(key string, value any) attribute.KeyValue {
switch typedVal := value.(type) {
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
case bool:
return attribute.Bool(key, typedVal)
case int:
return attribute.Int(key, typedVal)
case int8:
return attribute.Int(key, int(typedVal))
case int16:
return attribute.Int(key, int(typedVal))
case int32:
return attribute.Int(key, int(typedVal))
case int64:
return attribute.Int64(key, typedVal)
case uint:
return uintKV(key, typedVal)
case uint8:
return uintKV(key, uint(typedVal))
case uint16:
return uintKV(key, uint(typedVal))
case uint32:
return uintKV(key, uint(typedVal))
case uintptr:
return uint64KV(key, uint64(typedVal))
case uint64:
return uint64KV(key, uint64(typedVal))
case float32:
return attribute.Float64(key, float64(typedVal))
case float64:
return attribute.Float64(key, typedVal)
case complex64:
return attribute.String(key, complexToString(typedVal))
case complex128:
return attribute.String(key, complexToString(typedVal))
case string:
return attribute.String(key, typedVal)
case []bool:
return attribute.BoolSlice(key, typedVal)
case []int:
return attribute.IntSlice(key, typedVal)
case []int8:
return intSliceKV(key, typedVal)
case []int16:
return intSliceKV(key, typedVal)
case []int32:
return intSliceKV(key, typedVal)
case []int64:
return attribute.Int64Slice(key, typedVal)
case []uint:
return uintSliceKV(key, typedVal)
case []uint8:
return uintSliceKV(key, typedVal)
case []uint16:
return uintSliceKV(key, typedVal)
case []uint32:
return uintSliceKV(key, typedVal)
case []uintptr:
return uintSliceKV(key, typedVal)
case []uint64:
return uintSliceKV(key, typedVal)
case []float32:
floatSlice := make([]float64, len(typedVal))
for i := range typedVal {
floatSlice[i] = float64(typedVal[i])
}
return attribute.Float64Slice(key, floatSlice)
case []float64:
return attribute.Float64Slice(key, typedVal)
case []complex64:
return complexSliceKV(key, typedVal)
case []complex128:
return complexSliceKV(key, typedVal)
case []string:
return attribute.StringSlice(key, typedVal)
case fmt.Stringer:
return attribute.Stringer(key, typedVal)
default:
return attribute.String(key, fmt.Sprintf("unhandled attribute value: %+v", value))
}
}

func intSliceKV[N int8 | int16 | int32](key string, val []N) attribute.KeyValue {
intSlice := make([]int, len(val))
for i := range val {
intSlice[i] = int(val[i])
}
return attribute.IntSlice(key, intSlice)
}

func uintKV(key string, val uint) attribute.KeyValue {
if val > uint(math.MaxInt) {
return attribute.String(key, strconv.FormatUint(uint64(val), 10))
}
return attribute.Int(key, int(val))
}

func uintSliceKV[N uint | uint8 | uint16 | uint32 | uint64 | uintptr](key string, val []N) attribute.KeyValue {
strSlice := make([]string, len(val))
for i := range val {
strSlice[i] = strconv.FormatUint(uint64(val[i]), 10)
}
return bucketCounts, nil
return attribute.StringSlice(key, strSlice)
}

func uint64KV(key string, val uint64) attribute.KeyValue {
const maxInt64 = ^uint64(0) >> 1
if val > maxInt64 {
return attribute.String(key, strconv.FormatUint(val, 10))
}
return attribute.Int64(key, int64(val))
}

func complexSliceKV[N complex64 | complex128](key string, val []N) attribute.KeyValue {
strSlice := make([]string, len(val))
for i := range val {
strSlice[i] = complexToString(val[i])
}
return attribute.StringSlice(key, strSlice)
}

func complexToString[N complex64 | complex128](val N) string {
return strconv.FormatComplex(complex128(val), 'f', -1, 64)
}

// convertAttrs converts from OpenCensus attribute keys and values to an
Expand Down