From 8b1a75241d631b48fb640c21faa52192e8faa212 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Thu, 17 Mar 2022 07:25:23 -0400 Subject: [PATCH] Use json for writing floats as strings (#4934) * use json for writing floats as strings * avoid using reflection for floats * extra unit test * fix rebase Co-authored-by: Bogdan Drutu --- CHANGELOG.md | 1 + model/internal/pdata/common.go | 34 ++++++++++++++++++++++++++++- model/internal/pdata/common_test.go | 20 +++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47156365390..2a9206e179e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Remove `Type` funcs in pdata (#4933) - Remove all deprecated funcs/structs from v0.46.0 (#4995) +- AsString for pdata.AttributeValue now returns the JSON-encoded string of floats. (#4934) ### 🚩 Deprecations 🚩 diff --git a/model/internal/pdata/common.go b/model/internal/pdata/common.go index 8eb1e169c45..8f34744f05e 100644 --- a/model/internal/pdata/common.go +++ b/model/internal/pdata/common.go @@ -22,6 +22,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "math" "sort" "strconv" @@ -379,7 +380,7 @@ func (v Value) AsString() string { return strconv.FormatBool(v.BoolVal()) case ValueTypeDouble: - return strconv.FormatFloat(v.DoubleVal(), 'f', -1, 64) + return float64AsString(v.DoubleVal()) case ValueTypeInt: return strconv.FormatInt(v.IntVal(), 10) @@ -400,6 +401,37 @@ func (v Value) AsString() string { } } +// See https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/encoding/json/encode.go;l=585. +// This allows us 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 := Value{&orig.Value} diff --git a/model/internal/pdata/common_test.go b/model/internal/pdata/common_test.go index fbf6a971d3a..5d3516a488e 100644 --- a/model/internal/pdata/common_test.go +++ b/model/internal/pdata/common_test.go @@ -17,6 +17,7 @@ package pdata import ( "encoding/base64" "fmt" + "math" "strconv" "testing" @@ -780,6 +781,15 @@ func BenchmarkAttributeValue_SetIntVal(b *testing.B) { } } +func BenchmarkAttributeValueFloat_AsString(b *testing.B) { + av := NewValueDouble(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) @@ -1023,6 +1033,16 @@ func TestAsString(t *testing.T) { input: NewValueDouble(1.61803399), expected: "1.61803399", }, + { + name: "small float64", + input: NewValueDouble(.000000009), + expected: "9e-9", + }, + { + name: "bad float64", + input: NewValueDouble(math.Inf(1)), + expected: "json: unsupported value: +Inf", + }, { name: "boolean", input: NewValueBool(true),