diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 4db5c966c29..03cfdd4ac3e 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -12,9 +12,11 @@ import ( "regexp" "strconv" "strings" + "time" "golang.org/x/net/context/ctxhttp" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -123,7 +125,7 @@ func (e *GraphiteExecutor) DataQuery(ctx context.Context, dsInfo *models.DataSou return plugins.DataResponse{}, err } - data, err := e.parseResponse(res) + frames, err := e.toDataFrames(res) if err != nil { return plugins.DataResponse{}, err } @@ -131,19 +133,10 @@ func (e *GraphiteExecutor) DataQuery(ctx context.Context, dsInfo *models.DataSou result := plugins.DataResponse{ Results: make(map[string]plugins.DataQueryResult), } - queryRes := plugins.DataQueryResult{} - for _, series := range data { - queryRes.Series = append(queryRes.Series, plugins.DataTimeSeries{ - Name: series.Target, - Points: series.DataPoints, - }) - - if setting.Env == setting.Dev { - glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints)) - } + result.Results["A"] = plugins.DataQueryResult{ + RefID: "A", + Dataframes: plugins.NewDecodedDataFrames(frames), } - - result.Results["A"] = queryRes return result, nil } @@ -170,13 +163,39 @@ func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDT return nil, err } - for si := range data { - // Convert Response to timestamps MS - for pi, point := range data[si].DataPoints { - data[si].DataPoints[pi][1].Float64 = point[1].Float64 * 1000 + return data, nil +} + +func (e *GraphiteExecutor) toDataFrames(response *http.Response) (frames data.Frames, error error) { + responseData, err := e.parseResponse(response) + if err != nil { + return nil, err + } + + frames = data.Frames{} + for _, series := range responseData { + timeVector := make([]time.Time, 0, len(series.DataPoints)) + values := make([]*float64, 0, len(series.DataPoints)) + name := series.Target + + for _, dataPoint := range series.DataPoints { + var timestamp, value, err = parseDataTimePoint(dataPoint) + if err != nil { + return nil, err + } + timeVector = append(timeVector, timestamp) + values = append(values, value) + } + + frames = append(frames, data.NewFrame(name, + data.NewField("time", nil, timeVector), + data.NewField("value", series.Tags, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name}))) + + if setting.Env == setting.Dev { + glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints)) } } - return data, nil + return } func (e *GraphiteExecutor) createRequest(dsInfo *models.DataSource, data url.Values) (*http.Request, error) { @@ -242,3 +261,22 @@ func epochMStoGraphiteTime(tr plugins.DataTimeRange) (string, string, error) { return fmt.Sprintf("%d", from/1000), fmt.Sprintf("%d", to/1000), nil } + +/** + * Graphite should always return timestamp as a number but values might be nil when data is missing + */ +func parseDataTimePoint(dataTimePoint plugins.DataTimePoint) (time.Time, *float64, error) { + if !dataTimePoint[1].Valid { + return time.Time{}, nil, errors.New("failed to parse data point timestamp") + } + + timestamp := time.Unix(int64(dataTimePoint[1].Float64), 0).UTC() + + if dataTimePoint[0].Valid { + var value = new(float64) + *value = dataTimePoint[0].Float64 + return timestamp, value, nil + } else { + return timestamp, nil, nil + } +} diff --git a/pkg/tsdb/graphite/graphite_test.go b/pkg/tsdb/graphite/graphite_test.go index ecfd6e32762..ff60605de1e 100644 --- a/pkg/tsdb/graphite/graphite_test.go +++ b/pkg/tsdb/graphite/graphite_test.go @@ -1,9 +1,17 @@ package graphite import ( + "encoding/json" + "io/ioutil" + "net/http" + "reflect" + "strings" "testing" + "time" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFormatTimeRange(t *testing.T) { @@ -58,4 +66,37 @@ func TestFixIntervalFormat(t *testing.T) { assert.Equal(t, tc.expected, tr) }) } + + executor := &GraphiteExecutor{} + + t.Run("Converts response to data frames", func(*testing.T) { + body := ` + [ + { + "target": "target", + "tags": { "fooTag": "fooValue", "barTag": "barValue" }, + "datapoints": [[50, 1], [null, 2], [100, 3]] + } + ]` + a := 50.0 + b := 100.0 + expectedFrame := data.NewFrame("target", + data.NewField("time", nil, []time.Time{time.Unix(1, 0).UTC(), time.Unix(2, 0).UTC(), time.Unix(3, 0).UTC()}), + data.NewField("value", data.Labels{ + "fooTag": "fooValue", + "barTag": "barValue", + }, []*float64{&a, nil, &b}).SetConfig(&data.FieldConfig{DisplayNameFromDS: "target"}), + ) + expectedFrames := data.Frames{expectedFrame} + + httpResponse := &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(body))} + dataFrames, err := executor.toDataFrames(httpResponse) + + require.NoError(t, err) + if !reflect.DeepEqual(expectedFrames, dataFrames) { + expectedFramesJSON, _ := json.Marshal(expectedFrames) + dataFramesJSON, _ := json.Marshal(dataFrames) + t.Errorf("Data frames should have been equal but was, expected:\n%s\nactual:\n%s", expectedFramesJSON, dataFramesJSON) + } + }) } diff --git a/pkg/tsdb/graphite/types.go b/pkg/tsdb/graphite/types.go index 1c325b69eb1..9c7e0f08188 100644 --- a/pkg/tsdb/graphite/types.go +++ b/pkg/tsdb/graphite/types.go @@ -5,4 +5,5 @@ import "github.com/grafana/grafana/pkg/plugins" type TargetResponseDTO struct { Target string `json:"target"` DataPoints plugins.DataTimeSeriesPoints `json:"datapoints"` + Tags map[string]string `json:"tags"` }