diff --git a/model/pdata/common.go b/model/pdata/common.go index 87205879c1c..44a9529a4a0 100644 --- a/model/pdata/common.go +++ b/model/pdata/common.go @@ -22,6 +22,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "math" "sort" "strconv" @@ -379,8 +380,7 @@ func (a AttributeValue) AsString() string { return strconv.FormatBool(a.BoolVal()) case AttributeValueTypeDouble: - jsonStr, _ := json.Marshal(a.DoubleVal()) - return string(jsonStr) + return float64AsString(a.DoubleVal()) case AttributeValueTypeInt: return strconv.FormatInt(a.IntVal(), 10) @@ -401,6 +401,37 @@ func (a AttributeValue) AsString() string { } } +// See https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/encoding/json/encode.go;l=585. +// This allows up to avoid using reflection. +func float64AsString(f float64) string { + if math.IsInf(f, 0) || math.IsNaN(f) { + return fmt.Sprintf("json: unsupported value: %s", strconv.FormatFloat(f, 'g', -1, int(64))) + } + + // Convert as if by ES6 number to string conversion. + // This matches most other JSON generators. + // See golang.org/issue/6384 and golang.org/issue/14135. + // Like fmt %g, but the exponent cutoffs are different + // and exponents themselves are not padded to two digits. + scratch := [64]byte{} + b := scratch[:0] + abs := math.Abs(f) + fmt := byte('f') + if abs != 0 && (abs < 1e-6 || abs >= 1e21) { + fmt = 'e' + } + b = strconv.AppendFloat(b, f, fmt, -1, int(64)) + if fmt == 'e' { + // clean up e-09 to e-9 + n := len(b) + if n >= 4 && b[n-4] == 'e' && b[n-3] == '-' && b[n-2] == '0' { + b[n-2] = b[n-1] + b = b[:n-1] + } + } + return string(b) +} + func newAttributeKeyValueString(k string, v string) otlpcommon.KeyValue { orig := otlpcommon.KeyValue{Key: k} akv := AttributeValue{&orig.Value} diff --git a/model/pdata/common_test.go b/model/pdata/common_test.go index 4e16ee05074..832e0ac763e 100644 --- a/model/pdata/common_test.go +++ b/model/pdata/common_test.go @@ -755,6 +755,15 @@ func BenchmarkAttributeValue_SetIntVal(b *testing.B) { } } +func BenchmarkAttributeValueFloat_AsString(b *testing.B) { + av := NewAttributeValueDouble(2359871345.583429543) + + b.ResetTimer() + for n := 0; n < b.N; n++ { + av.AsString() + } +} + func BenchmarkAttributeMap_Range(b *testing.B) { const numElements = 20 rawOrig := make([]otlpcommon.KeyValue, numElements)