diff --git a/CHANGELOG.md b/CHANGELOG.md index 373a0e6b..19fa3c38 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 52f2a94e..869829d0 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 447bca0f..10298808 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) diff --git a/pkg/backends/datadog/flush_test.go b/pkg/backends/datadog/flush_test.go new file mode 100644 index 00000000..debfe327 --- /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) + } + }) + } +}