diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 38483d0af70..2f539e21064 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/components/null" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -28,7 +29,8 @@ var slog log.Logger // StackdriverExecutor executes queries for the Stackdriver datasource type StackdriverExecutor struct { - HTTPClient *http.Client + httpClient *http.Client + dsInfo *models.DataSource } // NewStackdriverExecutor initializes a http client @@ -39,7 +41,8 @@ func NewStackdriverExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, } return &StackdriverExecutor{ - HTTPClient: httpClient, + httpClient: httpClient, + dsInfo: dsInfo, }, nil } @@ -62,44 +65,7 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour } for _, query := range queries { - req, err := e.createRequest(ctx, dsInfo) - if err != nil { - return nil, err - } - - req.URL.RawQuery = query.Params.Encode() - slog.Info("tsdbQuery", "req.URL.RawQuery", req.URL.RawQuery) - - httpClient, err := dsInfo.GetHttpClient() - if err != nil { - return nil, err - } - - span, ctx := opentracing.StartSpanFromContext(ctx, "stackdriver query") - span.SetTag("target", query.Target) - span.SetTag("from", tsdbQuery.TimeRange.From) - span.SetTag("until", tsdbQuery.TimeRange.To) - span.SetTag("datasource_id", dsInfo.Id) - span.SetTag("org_id", dsInfo.OrgId) - - defer span.Finish() - - opentracing.GlobalTracer().Inject( - span.Context(), - opentracing.HTTPHeaders, - opentracing.HTTPHeadersCarrier(req.Header)) - - res, err := ctxhttp.Do(ctx, httpClient, req) - if err != nil { - return nil, err - } - - data, err := e.unmarshalResponse(res) - if err != nil { - return nil, err - } - - queryRes, err := e.parseResponse(data, query.RefID) + queryRes, err := e.executeQuery(ctx, query, tsdbQuery) if err != nil { return nil, err } @@ -153,6 +119,53 @@ func (e *StackdriverExecutor) parseQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd return stackdriverQueries, nil } +func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *StackdriverQuery, tsdbQuery *tsdb.TsdbQuery) (*tsdb.QueryResult, error) { + queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: query.RefID} + + req, err := e.createRequest(ctx, e.dsInfo) + if err != nil { + queryResult.Error = err + return queryResult, nil + } + + req.URL.RawQuery = query.Params.Encode() + queryResult.Meta.Set("rawQuery", req.URL.RawQuery) + + span, ctx := opentracing.StartSpanFromContext(ctx, "stackdriver query") + span.SetTag("target", query.Target) + span.SetTag("from", tsdbQuery.TimeRange.From) + span.SetTag("until", tsdbQuery.TimeRange.To) + span.SetTag("datasource_id", e.dsInfo.Id) + span.SetTag("org_id", e.dsInfo.OrgId) + + defer span.Finish() + + opentracing.GlobalTracer().Inject( + span.Context(), + opentracing.HTTPHeaders, + opentracing.HTTPHeadersCarrier(req.Header)) + + res, err := ctxhttp.Do(ctx, e.httpClient, req) + if err != nil { + queryResult.Error = err + return queryResult, nil + } + + data, err := e.unmarshalResponse(res) + if err != nil { + queryResult.Error = err + return queryResult, nil + } + + err = e.parseResponse(queryResult, data) + if err != nil { + queryResult.Error = err + return queryResult, nil + } + + return queryResult, nil +} + func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (StackDriverResponse, error) { body, err := ioutil.ReadAll(res.Body) defer res.Body.Close() @@ -161,24 +174,21 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (StackDriver } if res.StatusCode/100 != 2 { - slog.Info("Request failed", "status", res.Status, "body", string(body)) - return StackDriverResponse{}, fmt.Errorf("Request failed status: %v", res.Status) + slog.Error("Request failed", "status", res.Status, "body", string(body)) + return StackDriverResponse{}, fmt.Errorf(string(body)) } var data StackDriverResponse err = json.Unmarshal(body, &data) if err != nil { - slog.Info("Failed to unmarshal Stackdriver response", "error", err, "status", res.Status, "body", string(body)) + slog.Error("Failed to unmarshal Stackdriver response", "error", err, "status", res.Status, "body", string(body)) return StackDriverResponse{}, err } return data, nil } -func (e *StackdriverExecutor) parseResponse(data StackDriverResponse, queryRefID string) (*tsdb.QueryResult, error) { - queryRes := tsdb.NewQueryResult() - queryRes.RefId = queryRefID - +func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackDriverResponse) error { for _, series := range data.TimeSeries { points := make([]tsdb.TimePoint, 0) for _, point := range series.Points { @@ -195,7 +205,7 @@ func (e *StackdriverExecutor) parseResponse(data StackDriverResponse, queryRefID }) } - return queryRes, nil + return nil } func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) { @@ -227,7 +237,7 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo) - return req, err + return req, nil } func fixIntervalFormat(target string) string { diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 91fcfcd8645..228f0a48537 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -28,7 +28,7 @@ func TestStackdriver(t *testing.T) { { Model: simplejson.NewFromAny(map[string]interface{}{ "target": "target", - "metricType": "time_series", + "metricType": "a/metric/type", }), RefId: "A", }, @@ -44,52 +44,56 @@ func TestStackdriver(t *testing.T) { So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z") So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z") So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_NONE") - So(queries[0].Params["filter"][0], ShouldEqual, "time_series") + So(queries[0].Params["filter"][0], ShouldEqual, "a/metric/type") }) - Convey("Parse stackdriver response for data aggregated to one time series", func() { - var data StackDriverResponse + Convey("Parse stackdriver response in the time series format", func() { + Convey("when data from query aggregated to one time series", func() { + var data StackDriverResponse - jsonBody, err := ioutil.ReadFile("./test-data/1-series-response-agg-one-metric.json") - So(err, ShouldBeNil) - err = json.Unmarshal(jsonBody, &data) - So(err, ShouldBeNil) - So(len(data.TimeSeries), ShouldEqual, 1) + jsonBody, err := ioutil.ReadFile("./test-data/1-series-response-agg-one-metric.json") + So(err, ShouldBeNil) + err = json.Unmarshal(jsonBody, &data) + So(err, ShouldBeNil) + So(len(data.TimeSeries), ShouldEqual, 1) - res, err := executor.parseResponse(data, "A") - So(err, ShouldBeNil) - - So(len(res.Series), ShouldEqual, 1) - So(res.Series[0].Name, ShouldEqual, "serviceruntime.googleapis.com/api/request_count") - So(len(res.Series[0].Points), ShouldEqual, 3) - - So(res.Series[0].Points[0][0].Float64, ShouldEqual, 1.0666666666667) - So(res.Series[0].Points[1][0].Float64, ShouldEqual, 1.05) - So(res.Series[0].Points[2][0].Float64, ShouldEqual, 0.05) - }) - - Convey("Parse stackdriver response for data with no aggregation", func() { - var data StackDriverResponse - - jsonBody, err := ioutil.ReadFile("./test-data/2-series-response-no-agg.json") - So(err, ShouldBeNil) - err = json.Unmarshal(jsonBody, &data) - So(err, ShouldBeNil) - So(len(data.TimeSeries), ShouldEqual, 3) - - res, err := executor.parseResponse(data, "A") - So(err, ShouldBeNil) - - Convey("Should add labels to metric name", func() { - So(len(res.Series), ShouldEqual, 3) - So(res.Series[0].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-asia-east-1") - So(res.Series[1].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-europe-west-1") - So(res.Series[2].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-us-east-1") + res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} + err = executor.parseResponse(res, data) + So(err, ShouldBeNil) + So(len(res.Series), ShouldEqual, 1) + So(res.Series[0].Name, ShouldEqual, "serviceruntime.googleapis.com/api/request_count") So(len(res.Series[0].Points), ShouldEqual, 3) - So(res.Series[0].Points[0][0].Float64, ShouldEqual, 9.7730520330369) - So(res.Series[0].Points[1][0].Float64, ShouldEqual, 9.7323568146676) - So(res.Series[0].Points[2][0].Float64, ShouldEqual, 9.8566497180145) + + So(res.Series[0].Points[0][0].Float64, ShouldEqual, 1.0666666666667) + So(res.Series[0].Points[1][0].Float64, ShouldEqual, 1.05) + So(res.Series[0].Points[2][0].Float64, ShouldEqual, 0.05) + }) + + Convey("when data from query with no aggregation", func() { + var data StackDriverResponse + + jsonBody, err := ioutil.ReadFile("./test-data/2-series-response-no-agg.json") + So(err, ShouldBeNil) + err = json.Unmarshal(jsonBody, &data) + So(err, ShouldBeNil) + So(len(data.TimeSeries), ShouldEqual, 3) + + res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} + err = executor.parseResponse(res, data) + So(err, ShouldBeNil) + + Convey("Should add labels to metric name", func() { + So(len(res.Series), ShouldEqual, 3) + So(res.Series[0].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-asia-east-1") + So(res.Series[1].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-europe-west-1") + So(res.Series[2].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-us-east-1") + + So(len(res.Series[0].Points), ShouldEqual, 3) + So(res.Series[0].Points[0][0].Float64, ShouldEqual, 9.7730520330369) + So(res.Series[0].Points[1][0].Float64, ShouldEqual, 9.7323568146676) + So(res.Series[0].Points[2][0].Float64, ShouldEqual, 9.8566497180145) + }) }) }) }) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 7c863ab5697..231e21e8ec5 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -20,26 +20,27 @@ export default class StackdriverDatasource { const result = []; - try { - const { data } = await this.backendSrv.datasourceRequest({ - url: '/api/tsdb/query', - method: 'POST', - data: { - from: options.range.from.valueOf().toString(), - to: options.range.to.valueOf().toString(), - queries, - }, - }); + const { data } = await this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + from: options.range.from.valueOf().toString(), + to: options.range.to.valueOf().toString(), + queries, + }, + }); - if (data.results) { - Object['values'](data.results).forEach(queryRes => { - queryRes.series.forEach(series => { - result.push({ target: series.name, datapoints: series.points }); + if (data.results) { + Object['values'](data.results).forEach(queryRes => { + queryRes.series.forEach(series => { + result.push({ + target: series.name, + datapoints: series.points, + refId: queryRes.refId, + meta: queryRes.meta, }); }); - } - } catch (error) { - console.log(error); + }); } return { data: result }; diff --git a/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png b/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png new file mode 100644 index 00000000000..2084e85ed2b Binary files /dev/null and b/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png differ diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index f6df37a1414..021b1f63212 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -2,15 +2,49 @@
Project - +
Metric Type - + +
+
+
- \ No newline at end of file +
+
+ +
+
+ +
+
+
+
+
+ +
+
{{ctrl.lastQueryMeta.rawQueryString}}
+
+
+
+Help text for aliasing
+    
+
+
+
{{ctrl.lastQueryError}}
+
+ diff --git a/public/app/plugins/datasource/stackdriver/plugin.json b/public/app/plugins/datasource/stackdriver/plugin.json index 442adc02538..9216114b0e8 100644 --- a/public/app/plugins/datasource/stackdriver/plugin.json +++ b/public/app/plugins/datasource/stackdriver/plugin.json @@ -3,15 +3,23 @@ "type": "datasource", "id": "stackdriver", "metrics": true, - "alerting": false, + "alerting": true, "annotations": false, "queryOptions": { "maxDataPoints": true, "cacheTimeout": true }, "info": { - "description": "Data Source for Stackdriver", - "version": "1.0.0" + "description": "Google Stackdriver Datasource for Grafana", + "version": "1.0.0", + "logos": { + "small": "img/stackdriver_logo.png", + "large": "img/stackdriver_logo.png" + }, + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + } }, "routes": [ { diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 49f4b77e61d..a018bd4445e 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -2,6 +2,10 @@ import _ from 'lodash'; import { QueryCtrl } from 'app/plugins/sdk'; import appEvents from 'app/core/app_events'; +export interface QueryMeta { + rawQuery: string; + rawQueryString: string; +} export class StackdriverQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; target: { @@ -10,21 +14,31 @@ export class StackdriverQueryCtrl extends QueryCtrl { name: string; }; metricType: string; + refId: string; }; - defaultDropdownValue = 'select'; + defaultDropdownValue = 'Select metric'; defaults = { project: { id: 'default', name: 'loading project...', }, - metricType: this.defaultDropdownValue, + // metricType: this.defaultDropdownValue, }; + showHelp: boolean; + showLastQuery: boolean; + lastQueryMeta: QueryMeta; + lastQueryError?: string; + /** @ngInject */ constructor($scope, $injector) { super($scope, $injector); _.defaultsDeep(this.target, this.defaults); + + this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); + this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); + this.getCurrentProject().then(this.getMetricTypes.bind(this)); } @@ -67,4 +81,34 @@ export class StackdriverQueryCtrl extends QueryCtrl { return []; } } + + onDataReceived(dataList) { + this.lastQueryError = null; + this.lastQueryMeta = null; + + const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId }); + if (anySeriesFromQuery) { + this.lastQueryMeta = anySeriesFromQuery.meta; + this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery); + } + } + + onDataError(err) { + if (err.data && err.data.results) { + const queryRes = err.data.results[this.target.refId]; + if (queryRes) { + this.lastQueryMeta = queryRes.meta; + this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery); + + let jsonBody; + try { + jsonBody = JSON.parse(queryRes.error); + } catch { + this.lastQueryError = queryRes.error; + } + + this.lastQueryError = jsonBody.error.message; + } + } + } }