diff --git a/src/query/api/v1/handler/prometheus/native/common.go b/src/query/api/v1/handler/prometheus/native/common.go index d1827e0933..d7e4ca65f7 100644 --- a/src/query/api/v1/handler/prometheus/native/common.go +++ b/src/query/api/v1/handler/prometheus/native/common.go @@ -240,12 +240,37 @@ func parseQuery(r *http.Request) (string, error) { return queries[0], nil } +func filterNaNSeries(series []*ts.Series) []*ts.Series { + filtered := series[:0] + for _, s := range series { + dps := s.Values().Datapoints() + hasVal := false + for _, dp := range dps { + if !math.IsNaN(dp.Value) { + hasVal = true + break + } + } + + if hasVal { + filtered = append(filtered, s) + } + } + + return filtered +} + func renderResultsJSON( w io.Writer, series []*ts.Series, params models.RequestParams, keepNans bool, ) { + // NB: if dropping NaNs, drop series with only NaNs from output entirely. + if !keepNans { + series = filterNaNSeries(series) + } + jw := json.NewWriter(w) jw.BeginObject() diff --git a/src/query/api/v1/handler/prometheus/native/common_test.go b/src/query/api/v1/handler/prometheus/native/common_test.go index 0781318b01..ac271f8f3c 100644 --- a/src/query/api/v1/handler/prometheus/native/common_test.go +++ b/src/query/api/v1/handler/prometheus/native/common_test.go @@ -37,6 +37,7 @@ import ( "github.com/m3db/m3/src/query/util/logging" xtest "github.com/m3db/m3/src/x/test" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -351,14 +352,6 @@ func TestRenderResultsJSONWithDroppedNaNs(t *testing.T) { ] ], "step_size_ms": 10000 - }, - { - "metric": { - "biz": "baz", - "qux": "qaz" - }, - "values": [], - "step_size_ms": 10000 } ] } @@ -429,3 +422,40 @@ func mustPrettyJSON(t *testing.T, str string) string { require.NoError(t, err) return string(pretty) } + +func TestSanitizeSeries(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + nan := math.NaN() + testData := []struct { + name string + data []float64 + }{ + {"1", []float64{nan, nan, nan, nan}}, + {"2", []float64{nan, nan, nan, 1}}, + {"3", []float64{nan, nan, nan, nan}}, + {"4", []float64{nan, nan, 1, nan}}, + {"5", []float64{1, 1, 1, 1}}, + {"6", []float64{nan, nan, nan, nan}}, + } + + series := make([]*ts.Series, 0, len(testData)) + tags := models.NewTags(0, models.NewTagOptions()) + for _, d := range testData { + vals := ts.NewMockValues(ctrl) + dps := make(ts.Datapoints, 0, len(d.data)) + for _, p := range d.data { + dps = append(dps, ts.Datapoint{Value: p}) + } + + vals.EXPECT().Datapoints().Return(dps) + series = append(series, ts.NewSeries([]byte(d.name), vals, tags)) + } + + series = filterNaNSeries(series) + require.Equal(t, 3, len(series)) + assert.Equal(t, "2", string(series[0].Name())) + assert.Equal(t, "4", string(series[1].Name())) + assert.Equal(t, "5", string(series[2].Name())) +} diff --git a/src/query/generated/mocks/generate.go b/src/query/generated/mocks/generate.go index 16caadda00..5d2fb29594 100644 --- a/src/query/generated/mocks/generate.go +++ b/src/query/generated/mocks/generate.go @@ -22,6 +22,7 @@ //go:generate sh -c "mockgen -package=downsample $PACKAGE/src/cmd/services/m3coordinator/downsample Downsampler,MetricsAppender,SamplesAppender | genclean -pkg $PACKAGE/src/cmd/services/m3coordinator/downsample -out $GOPATH/src/$PACKAGE/src/cmd/services/m3coordinator/downsample/downsample_mock.go" //go:generate sh -c "mockgen -package=storage -destination=$GOPATH/src/$PACKAGE/src/query/storage/storage_mock.go $PACKAGE/src/query/storage Storage" //go:generate sh -c "mockgen -package=m3 -destination=$GOPATH/src/$PACKAGE/src/query/storage/m3/m3_mock.go $PACKAGE/src/query/storage/m3 Storage" +//go:generate sh -c "mockgen -package=ts -destination=$GOPATH/src/$PACKAGE/src/query/ts/ts_mock.go $PACKAGE/src/query/ts Values" //go:generate sh -c "mockgen -package=block -destination=$GOPATH/src/$PACKAGE/src/query/block/block_mock.go $PACKAGE/src/query/block Block,StepIter,SeriesIter,Builder,Step,UnconsolidatedBlock,UnconsolidatedStepIter,UnconsolidatedSeriesIter,UnconsolidatedStep" //go:generate sh -c "mockgen -package=ingest -destination=$GOPATH/src/$PACKAGE/src/cmd/services/m3coordinator/ingest/write_mock.go $PACKAGE/src/cmd/services/m3coordinator/ingest DownsamplerAndWriter" //go:generate sh -c "mockgen -package=transform -destination=$GOPATH/src/$PACKAGE/src/query/executor/transform/types_mock.go $PACKAGE/src/query/executor/transform OpNode" diff --git a/src/query/ts/ts_mock.go b/src/query/ts/ts_mock.go new file mode 100644 index 0000000000..a932cd94e0 --- /dev/null +++ b/src/query/ts/ts_mock.go @@ -0,0 +1,141 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/m3db/m3/src/query/ts (interfaces: Values) + +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Package ts is a generated GoMock package. +package ts + +import ( + "reflect" + "time" + + "github.com/m3db/m3/src/query/models" + + "github.com/golang/mock/gomock" +) + +// MockValues is a mock of Values interface +type MockValues struct { + ctrl *gomock.Controller + recorder *MockValuesMockRecorder +} + +// MockValuesMockRecorder is the mock recorder for MockValues +type MockValuesMockRecorder struct { + mock *MockValues +} + +// NewMockValues creates a new mock instance +func NewMockValues(ctrl *gomock.Controller) *MockValues { + mock := &MockValues{ctrl: ctrl} + mock.recorder = &MockValuesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockValues) EXPECT() *MockValuesMockRecorder { + return m.recorder +} + +// AlignToBounds mocks base method +func (m *MockValues) AlignToBounds(arg0 models.Bounds, arg1 time.Duration) []Datapoints { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AlignToBounds", arg0, arg1) + ret0, _ := ret[0].([]Datapoints) + return ret0 +} + +// AlignToBounds indicates an expected call of AlignToBounds +func (mr *MockValuesMockRecorder) AlignToBounds(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AlignToBounds", reflect.TypeOf((*MockValues)(nil).AlignToBounds), arg0, arg1) +} + +// AlignToBoundsNoWriteForward mocks base method +func (m *MockValues) AlignToBoundsNoWriteForward(arg0 models.Bounds, arg1 time.Duration) []Datapoints { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AlignToBoundsNoWriteForward", arg0, arg1) + ret0, _ := ret[0].([]Datapoints) + return ret0 +} + +// AlignToBoundsNoWriteForward indicates an expected call of AlignToBoundsNoWriteForward +func (mr *MockValuesMockRecorder) AlignToBoundsNoWriteForward(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AlignToBoundsNoWriteForward", reflect.TypeOf((*MockValues)(nil).AlignToBoundsNoWriteForward), arg0, arg1) +} + +// DatapointAt mocks base method +func (m *MockValues) DatapointAt(arg0 int) Datapoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DatapointAt", arg0) + ret0, _ := ret[0].(Datapoint) + return ret0 +} + +// DatapointAt indicates an expected call of DatapointAt +func (mr *MockValuesMockRecorder) DatapointAt(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DatapointAt", reflect.TypeOf((*MockValues)(nil).DatapointAt), arg0) +} + +// Datapoints mocks base method +func (m *MockValues) Datapoints() []Datapoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Datapoints") + ret0, _ := ret[0].([]Datapoint) + return ret0 +} + +// Datapoints indicates an expected call of Datapoints +func (mr *MockValuesMockRecorder) Datapoints() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Datapoints", reflect.TypeOf((*MockValues)(nil).Datapoints)) +} + +// Len mocks base method +func (m *MockValues) Len() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Len") + ret0, _ := ret[0].(int) + return ret0 +} + +// Len indicates an expected call of Len +func (mr *MockValuesMockRecorder) Len() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Len", reflect.TypeOf((*MockValues)(nil).Len)) +} + +// ValueAt mocks base method +func (m *MockValues) ValueAt(arg0 int) float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValueAt", arg0) + ret0, _ := ret[0].(float64) + return ret0 +} + +// ValueAt indicates an expected call of ValueAt +func (mr *MockValuesMockRecorder) ValueAt(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValueAt", reflect.TypeOf((*MockValues)(nil).ValueAt), arg0) +}