package influxdb import ( "encoding/json" "io" "io/ioutil" "strings" "testing" "time" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/plugins" "github.com/stretchr/testify/require" ) func prepare(text string) io.ReadCloser { return ioutil.NopCloser(strings.NewReader(text)) } // nolint:staticcheck // plugins.DataQueryResult deprecated func decodedFrames(t *testing.T, result plugins.DataQueryResult) data.Frames { decoded, err := result.Dataframes.Decoded() require.NoError(t, err) return decoded } // nolint:staticcheck // plugins.DataQueryResult deprecated func assertSeriesName(t *testing.T, result plugins.DataQueryResult, index int, name string) { decoded := decodedFrames(t, result) frame := decoded[index] require.Equal(t, frame.Name, name) // the current version of the alerting-code does not use the dataframe-name // when generating the metric-names for the alerts. // instead, it goes through multiple attributes on the Field. // we use the `field.Config.DisplayNameFromDS` attribute. valueFieldConfig := frame.Fields[1].Config require.NotNil(t, valueFieldConfig) require.Equal(t, valueFieldConfig.DisplayNameFromDS, name) } func TestInfluxdbResponseParser(t *testing.T) { t.Run("Influxdb response parser should handle invalid JSON", func(t *testing.T) { parser := &ResponseParser{} response := `{ invalid }` query := &Query{} result := parser.Parse(prepare(response), query) require.Nil(t, result.Dataframes) require.Error(t, result.Error) }) t.Run("Influxdb response parser should parse everything normally", func(t *testing.T) { parser := &ResponseParser{} response := ` { "results": [ { "series": [ { "name": "cpu", "columns": ["time","mean","sum"], "tags": {"datacenter": "America"}, "values": [ [111,222,333], [111,222,333], [111,null,333] ] } ] } ] } ` query := &Query{} result := parser.Parse(prepare(response), query) decoded := decodedFrames(t, result) require.Len(t, decoded, 2) frame1 := decoded[0] frame2 := decoded[1] assertSeriesName(t, result, 0, "cpu.mean { datacenter: America }") assertSeriesName(t, result, 1, "cpu.sum { datacenter: America }") require.Len(t, frame1.Fields, 2) require.Len(t, frame2.Fields, 2) require.Equal(t, frame1.Fields[0].Len(), 3) require.Equal(t, frame1.Fields[1].Len(), 3) require.Equal(t, frame2.Fields[0].Len(), 3) require.Equal(t, frame2.Fields[1].Len(), 3) require.Equal(t, *frame1.Fields[1].At(1).(*float64), 222.0) require.Equal(t, *frame2.Fields[1].At(1).(*float64), 333.0) require.Nil(t, frame1.Fields[1].At(2)) }) t.Run("Influxdb response parser with invalid value-format", func(t *testing.T) { parser := &ResponseParser{} response := ` { "results": [ { "series": [ { "name": "cpu", "columns": ["time","mean"], "values": [ [100,50], [101,"hello"], [102,52] ] } ] } ] } ` query := &Query{} result := parser.Parse(prepare(response), query) // the current behavior is that we do not report an error, we turn the invalid value into `nil` require.Nil(t, result.Error) require.Equal(t, result.ErrorString, "") decoded := decodedFrames(t, result) require.Len(t, decoded, 1) frame := decoded[0] require.Len(t, frame.Fields, 2) field1 := frame.Fields[0] field2 := frame.Fields[1] require.Equal(t, field1.Len(), 3) require.Equal(t, field2.Len(), 3) require.Equal(t, *field2.At(0).(*float64), 50.0) require.Nil(t, field2.At(1)) require.Equal(t, *field2.At(2).(*float64), 52.0) }) t.Run("Influxdb response parser with invalid timestamp-format", func(t *testing.T) { parser := &ResponseParser{} response := ` { "results": [ { "series": [ { "name": "cpu", "columns": ["time","mean"], "values": [ [100,50], ["hello",51], ["hello","hello"], [102,52] ] } ] } ] } ` query := &Query{} result := parser.Parse(prepare(response), query) // the current behavior is that we do not report an error, we skip the item with the invalid timestamp require.Nil(t, result.Error) require.Equal(t, result.ErrorString, "") decoded := decodedFrames(t, result) require.Len(t, decoded, 1) frame := decoded[0] require.Len(t, frame.Fields, 2) field1 := frame.Fields[0] field2 := frame.Fields[1] require.Equal(t, field1.Len(), 2) require.Equal(t, field2.Len(), 2) require.Equal(t, *field2.At(0).(*float64), 50.0) require.Equal(t, *field2.At(1).(*float64), 52.0) }) t.Run("Influxdb response parser with alias", func(t *testing.T) { parser := &ResponseParser{} response := ` { "results": [ { "series": [ { "name": "cpu.upc", "columns": ["time","mean","sum"], "tags": { "datacenter": "America", "dc.region.name": "Northeast", "cluster-name": "Cluster", "/cluster/name/": "Cluster/", "@cluster@name@": "Cluster@" }, "values": [ [111,222,333] ] } ] } ] } ` query := &Query{Alias: "series alias"} result := parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "series alias") query = &Query{Alias: "alias $m $measurement", Measurement: "10m"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias 10m 10m") query = &Query{Alias: "alias $col", Measurement: "10m"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias mean") assertSeriesName(t, result, 1, "alias sum") query = &Query{Alias: "alias $tag_datacenter"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias America") query = &Query{Alias: "alias $1"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias upc") query = &Query{Alias: "alias $5"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias $5") query = &Query{Alias: "series alias"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "series alias") query = &Query{Alias: "alias [[m]] [[measurement]]", Measurement: "10m"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias 10m 10m") query = &Query{Alias: "alias [[col]]", Measurement: "10m"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias mean") assertSeriesName(t, result, 1, "alias sum") query = &Query{Alias: "alias [[tag_datacenter]]"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias America") query = &Query{Alias: "alias [[tag_dc.region.name]]"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias Northeast") query = &Query{Alias: "alias [[tag_cluster-name]]"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias Cluster") query = &Query{Alias: "alias [[tag_/cluster/name/]]"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias Cluster/") query = &Query{Alias: "alias [[tag_@cluster@name@]]"} result = parser.Parse(prepare(response), query) assertSeriesName(t, result, 0, "alias Cluster@") }) t.Run("Influxdb response parser with errors", func(t *testing.T) { parser := &ResponseParser{} response := ` { "results": [ { "series": [ { "name": "cpu", "columns": ["time","mean","sum"], "tags": {"datacenter": "America"}, "values": [ [111,222,333], [111,222,333], [111,null,333] ] } ] }, { "error": "query-timeout limit exceeded" } ] } ` query := &Query{} result := parser.Parse(prepare(response), query) decoded := decodedFrames(t, result) require.Len(t, decoded, 2) require.Equal(t, decoded[0].Fields[0].Len(), 3) require.Equal(t, decoded[0].Fields[1].Len(), 3) require.Equal(t, decoded[1].Fields[0].Len(), 3) require.Equal(t, decoded[1].Fields[1].Len(), 3) require.EqualError(t, result.Error, "query-timeout limit exceeded") }) t.Run("Influxdb response parser with top-level error", func(t *testing.T) { parser := &ResponseParser{} response := ` { "error": "error parsing query: found THING" } ` query := &Query{} result := parser.Parse(prepare(response), query) require.Nil(t, result.Dataframes) require.EqualError(t, result.Error, "error parsing query: found THING") }) t.Run("Influxdb response parser parseValue nil", func(t *testing.T) { value := parseValue(nil) require.Nil(t, value) }) t.Run("Influxdb response parser parseValue valid JSON.number", func(t *testing.T) { value := parseValue(json.Number("95.4")) require.Equal(t, *value, 95.4) }) t.Run("Influxdb response parser parseValue invalid type", func(t *testing.T) { value := parseValue("95.4") require.Nil(t, value) }) t.Run("Influxdb response parser parseTimestamp valid JSON.number", func(t *testing.T) { // currently we use seconds-precision with influxdb, so the test works with that. // if we change this to for example milliseconds-precision, the tests will have to change. timestamp, err := parseTimestamp(json.Number("1609556645")) require.NoError(t, err) require.Equal(t, timestamp.Format(time.RFC3339), "2021-01-02T03:04:05Z") }) t.Run("Influxdb response parser parseValue invalid type", func(t *testing.T) { _, err := parseTimestamp("hello") require.Error(t, err) }) }