Repository: atlassian/gostatsd
Pull request #343 (2020-09-09 06:47:07)

Coerce non-numeric values to numeric in the datadog backend
In order to address the case where data is dropped from the datadog backend if the aggregation result is a non-numeric value (NaN, +Inf, -Inf), this PR modifies the component's behaviour to coerce such values into a numeric. This serves as a basic fix to #342. 

* Inf+ --> float.Max
* Inf- --> float.Min
* NaN --> -1

NaN's conversion to -1 was chosen because negative values are (anecdotally) less frequent compared to zero counters.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 373a0e6..19fa3c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+28.3.0
+------
+- Rather than dropping data points, the Datadog backend will coerce non-numeric values resulting from aggregation to numeric.
+`NaN` is converted to -1, `+Inf` to float64 maximum, and `-Inf` to float64 minimum.
+
 28.2.1
 ------
 - Document how semver is used in the codebase.
diff --git a/go.sum b/go.sum
index 52f2a94..869829d 100644
--- a/go.sum
+++ b/go.sum
@@ -198,6 +198,7 @@ github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
 github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a h1:GmsqmapfzSJkm28dhRoHz2tLRbJmqhU86IPgBtN3mmk=
 github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
diff --git a/pkg/backends/datadog/flush.go b/pkg/backends/datadog/flush.go
index 447bca0..1029880 100644
--- a/pkg/backends/datadog/flush.go
+++ b/pkg/backends/datadog/flush.go
@@ -51,21 +51,31 @@ func (f *flush) addMetricf(metricType metricType, value float64, source gostatsd
 }
 
 // addMetric adds a metric to the series.
+// If the value is non-numeric (in the case of NaN and Inf values), the value is coerced into a numeric value.
 func (f *flush) addMetric(metricType metricType, value float64, source gostatsd.Source, tags gostatsd.Tags, name string) {
-	if math.IsInf(value, 1) || math.IsInf(value, -1) || math.IsNaN(value) {
-		// The value can not be represented within the JSON payload so it is to be discarded.
-		return
-	}
 	f.ts.Series = append(f.ts.Series, metric{
 		Host:     string(source),
 		Interval: f.flushIntervalSec,
 		Metric:   name,
-		Points:   [1]point{{f.timestamp, value}},
+		Points:   [1]point{{f.timestamp, coerceToNumeric(value)}},
 		Tags:     tags,
 		Type:     metricType,
 	})
 }
 
+// coerceToNumeric will convert non-numeric NaN and Inf values to a numeric value.
+// If v is a numeric, the same value is returned.
+func coerceToNumeric(v float64) float64 {
+	if math.IsNaN(v) {
+		return -1
+	} else if math.IsInf(v, 1) {
+		return math.MaxFloat64
+	} else if math.IsInf(v, -1) {
+		return -math.MaxFloat64
+	}
+	return v
+}
+
 func (f *flush) maybeFlush() {
 	if uint(len(f.ts.Series))+20 >= f.metricsPerBatch { // flush before it reaches max size and grows the slice
 		f.cb(f.ts)


Test patch:
diff --git a/pkg/backends/datadog/flush_test.go b/pkg/backends/datadog/flush_test.go
new file mode 100644
index 0000000..debfe32
--- /dev/null
+++ b/pkg/backends/datadog/flush_test.go
@@ -0,0 +1,31 @@
+package datadog
+
+import (
+	"math"
+	"testing"
+)
+
+func TestCoerceToNumeric(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name string
+		arg  float64
+		want float64
+	}{
+		{"NaN should return 0", math.NaN(), -1},
+		{"+Inf should return maximum float value", math.Inf(+1), math.MaxFloat64},
+		{"-Inf should return minimum float value", math.Inf(-1), -math.MaxFloat64},
+		{"Numeric value should return unchanged", 0, 0},
+		{"Numeric value should return unchanged", 12_345, 12_345},
+		{"Numeric value should return unchanged", -12_345, -12_345},
+		{"Numeric value should return unchanged", math.MaxFloat64, math.MaxFloat64},
+		{"-Inf should return minimum float value", -math.MaxFloat64, -math.MaxFloat64},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := coerceToNumeric(tt.arg); got != tt.want {
+				t.Errorf("coerceToNumeric() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}