From c70c9982eee028a9054deb3eecaed31036425091 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Fri, 13 Oct 2023 16:06:41 +0000 Subject: [PATCH] expand set of translated exemplar attributes --- bridge/opencensus/internal/ocmetric/metric.go | 136 +++++++++-- .../internal/ocmetric/metric_test.go | 224 +++++++++++++----- 2 files changed, 278 insertions(+), 82 deletions(-) diff --git a/bridge/opencensus/internal/ocmetric/metric.go b/bridge/opencensus/internal/ocmetric/metric.go index 08cfbebae313..b55874f3a669 100644 --- a/bridge/opencensus/internal/ocmetric/metric.go +++ b/bridge/opencensus/internal/ocmetric/metric.go @@ -19,6 +19,7 @@ import ( "fmt" "reflect" "sort" + "strconv" ocmetricdata "go.opencensus.io/metric/metricdata" octrace "go.opencensus.io/trace" @@ -28,13 +29,12 @@ 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") - 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") - errInvalidExemplarAttachmentValue = errors.New("exemplar attachment is not a supported OpenTelemetry attribute type") + errAggregationType = errors.New("unsupported OpenCensus aggregation type") + errMismatchedValueTypes = errors.New("wrong value type for data point") + 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. @@ -206,12 +206,7 @@ func convertExemplar(ocExemplar *ocmetricdata.Exemplar) (metricdata.Exemplar[flo exemplar.SpanID = sc.SpanID[:] exemplar.TraceID = sc.TraceID[:] default: - kv := convertKV(k, v) - if !kv.Valid() { - err = errors.Join(err, fmt.Errorf("%w; type: %v", errInvalidExemplarAttachmentValue, reflect.TypeOf(v))) - continue - } - exemplar.FilteredAttributes = append(exemplar.FilteredAttributes, kv) + exemplar.FilteredAttributes = append(exemplar.FilteredAttributes, convertKV(k, v)) } } sortable := attribute.Sortable(exemplar.FilteredAttributes) @@ -224,28 +219,125 @@ func convertKV(key string, value any) attribute.KeyValue { switch typedVal := value.(type) { case bool: return attribute.Bool(key, typedVal) - case []bool: - return attribute.BoolSlice(key, typedVal) case int: return attribute.Int(key, typedVal) - case []int: - return attribute.IntSlice(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 []int64: - return attribute.Int64Slice(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 []float64: - return attribute.Float64Slice(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 { + const maxInt = ^uint(0) >> 1 + if val > maxInt { + return uint64KV(key, uint64(val)) + } + 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 attribute.KeyValue{} + 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 diff --git a/bridge/opencensus/internal/ocmetric/metric_test.go b/bridge/opencensus/internal/ocmetric/metric_test.go index b6c5161e8d15..722c8ba7e015 100644 --- a/bridge/opencensus/internal/ocmetric/metric_test.go +++ b/bridge/opencensus/internal/ocmetric/metric_test.go @@ -16,9 +16,13 @@ package internal // import "go.opentelemetry.io/otel/bridge/opencensus/opencensu import ( "errors" + "fmt" + "math" + "reflect" "testing" "time" + "github.com/stretchr/testify/assert" ocmetricdata "go.opencensus.io/metric/metricdata" octrace "go.opencensus.io/trace" @@ -86,17 +90,7 @@ func TestConvertMetrics(t *testing.T) { TraceID: octrace.TraceID([16]byte{1}), SpanID: octrace.SpanID([8]byte{2}), }, - "bool": true, - "boolslice": []bool{true, false}, - "int": 10, - "intslice": []int{10, 20}, - "int64": int64(10), - "int64slice": []int64{10, 20}, - "float64": float64(10.0), - "float64slice": []float64{10.0, 20.0}, - "string": "string", - "stringslice": []string{"string", "slice"}, - "stringer": fakeStringer("stringer"), + "bool": true, }, }, }, @@ -323,16 +317,6 @@ func TestConvertMetrics(t *testing.T) { SpanID: []byte{2, 0, 0, 0, 0, 0, 0, 0}, FilteredAttributes: []attribute.KeyValue{ attribute.Bool("bool", true), - attribute.BoolSlice("boolslice", []bool{true, false}), - attribute.Float64("float64", 10.0), - attribute.Float64Slice("float64slice", []float64{10.0, 20.0}), - attribute.Int("int", 10), - attribute.Int64("int64", 10), - attribute.Int64Slice("int64slice", []int64{10, 20}), - attribute.IntSlice("intslice", []int{10, 20}), - attribute.String("string", "string"), - attribute.Stringer("stringer", fakeStringer("stringer")), - attribute.StringSlice("stringslice", []string{"string", "slice"}), }, }, { @@ -687,45 +671,6 @@ func TestConvertMetrics(t *testing.T) { }, }, expectedErr: errInvalidExemplarSpanContext, - }, { - desc: "histogram with invalid exemplar attachment", - input: []*ocmetricdata.Metric{ - { - Descriptor: ocmetricdata.Descriptor{ - Name: "foo.com/histogram-a", - Description: "a testing histogram", - Unit: ocmetricdata.UnitDimensionless, - Type: ocmetricdata.TypeCumulativeDistribution, - }, - TimeSeries: []*ocmetricdata.TimeSeries{ - { - Points: []ocmetricdata.Point{ - ocmetricdata.NewDistributionPoint(endTime1, &ocmetricdata.Distribution{ - Count: 8, - Sum: 100.0, - BucketOptions: &ocmetricdata.BucketOptions{ - Bounds: []float64{1.0, 2.0, 3.0}, - }, - Buckets: []ocmetricdata.Bucket{ - { - Count: 1, - Exemplar: &ocmetricdata.Exemplar{ - Value: 0.8, - Timestamp: exemplarTime, - Attachments: map[string]interface{}{ - "invalid attachment": metricdata.Histogram[float64]{}, - }, - }, - }, - }, - }), - }, - StartTime: startTime, - }, - }, - }, - }, - expectedErr: errInvalidExemplarAttachmentValue, }, { desc: "sum with non-sum datapoint type", input: []*ocmetricdata.Metric{ @@ -854,3 +799,162 @@ type fakeStringer string func (f fakeStringer) String() string { return string(f) } + +func TestConvertKV(t *testing.T) { + key := "foo" + for _, tt := range []struct { + value any + expected attribute.Value + }{ + { + value: bool(true), + expected: attribute.BoolValue(true), + }, + { + value: []bool{true, false}, + expected: attribute.BoolSliceValue([]bool{true, false}), + }, + { + value: int(10), + expected: attribute.IntValue(10), + }, + { + value: []int{10, 20}, + expected: attribute.IntSliceValue([]int{10, 20}), + }, + { + value: int8(10), + expected: attribute.IntValue(10), + }, + { + value: []int8{10, 20}, + expected: attribute.IntSliceValue([]int{10, 20}), + }, + { + value: int16(10), + expected: attribute.IntValue(10), + }, + { + value: []int16{10, 20}, + expected: attribute.IntSliceValue([]int{10, 20}), + }, + { + value: int32(10), + expected: attribute.IntValue(10), + }, + { + value: []int32{10, 20}, + expected: attribute.IntSliceValue([]int{10, 20}), + }, + { + value: int64(10), + expected: attribute.Int64Value(10), + }, + { + value: []int64{10, 20}, + expected: attribute.Int64SliceValue([]int64{10, 20}), + }, + { + value: uint(10), + expected: attribute.IntValue(10), + }, + { + value: []uint{10, 20}, + expected: attribute.StringSliceValue([]string{"10", "20"}), + }, + { + value: uint8(10), + expected: attribute.IntValue(10), + }, + { + value: []uint8{10, 20}, + expected: attribute.StringSliceValue([]string{"10", "20"}), + }, + { + value: uint16(10), + expected: attribute.IntValue(10), + }, + { + value: []uint16{10, 20}, + expected: attribute.StringSliceValue([]string{"10", "20"}), + }, + { + value: uint32(10), + expected: attribute.IntValue(10), + }, + { + value: uint32(math.MaxUint32), + expected: attribute.Int64Value(math.MaxUint32), + }, + { + value: []uint32{10, 20}, + expected: attribute.StringSliceValue([]string{"10", "20"}), + }, + { + value: uint64(10), + expected: attribute.Int64Value(10), + }, + { + value: uint64(math.MaxUint64), + expected: attribute.StringValue("18446744073709551615"), + }, + { + value: []uint64{10, 20}, + expected: attribute.StringSliceValue([]string{"10", "20"}), + }, + { + value: uintptr(10), + expected: attribute.Int64Value(10), + }, + { + value: []uintptr{10, 20}, + expected: attribute.StringSliceValue([]string{"10", "20"}), + }, + { + value: float64(10), + expected: attribute.Float64Value(10), + }, + { + value: []float64{10, 20}, + expected: attribute.Float64SliceValue([]float64{10, 20}), + }, + { + value: complex64(10), + expected: attribute.StringValue("(10+0i)"), + }, + { + value: []complex64{10, 20}, + expected: attribute.StringSliceValue([]string{"(10+0i)", "(20+0i)"}), + }, + { + value: complex128(10), + expected: attribute.StringValue("(10+0i)"), + }, + { + value: []complex128{10, 20}, + expected: attribute.StringSliceValue([]string{"(10+0i)", "(20+0i)"}), + }, + { + value: "string", + expected: attribute.StringValue("string"), + }, + { + value: []string{"string", "slice"}, + expected: attribute.StringSliceValue([]string{"string", "slice"}), + }, + { + value: fakeStringer("stringer"), + expected: attribute.StringValue("stringer"), + }, + { + value: metricdata.Histogram[float64]{}, + expected: attribute.StringValue("unhandled attribute value: {DataPoints:[] Temporality:undefinedTemporality}"), + }, + } { + t.Run(fmt.Sprintf("%v(%+v)", reflect.TypeOf(tt.value), tt.value), func(t *testing.T) { + got := convertKV(key, tt.value) + assert.Equal(t, key, string(got.Key)) + assert.Equal(t, tt.expected, got.Value) + }) + } +}