From 0b5783563ed9bbfa0d72a72f2061b10b59ca034b Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 11 Sep 2018 22:41:24 +0200 Subject: [PATCH] stackdriver: better error handling and show query metadata If the Stackdriver returns an error, show that error in the query editor. Also, allow the user to see the raw querystring that was sent to google (for troubleshooting). --- pkg/tsdb/stackdriver/stackdriver.go | 108 ++++++++++-------- pkg/tsdb/stackdriver/stackdriver_test.go | 86 +++++++------- .../datasource/stackdriver/datasource.ts | 35 +++--- .../stackdriver/img/stackdriver_logo.png | Bin 0 -> 1988 bytes .../stackdriver/partials/query.editor.html | 44 ++++++- .../datasource/stackdriver/plugin.json | 14 ++- .../datasource/stackdriver/query_ctrl.ts | 48 +++++++- 7 files changed, 218 insertions(+), 117 deletions(-) create mode 100644 public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png 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 0000000000000000000000000000000000000000..2084e85ed2bbd9f452165a54bf37cc190ce43e6f GIT binary patch literal 1988 zcmZ`)c{J307ynIT#xzXiF%@O2hk3G$WC>##``RFsC0h(r6fuNB)+}$Dl*Vpko9q#< zEJHmB5ysdl%04oYhKlfv_dNf;f82Y|=X>twe($;GbIvE(8fSuli^2f_K$w{tUgFl! zgMc04=Kn&UCUDE)08_^x0O0%MAV5HN?g{Rr2+_!aXzTAyyyFpw2lTxBui+)lZhH9O zFX27B!fwCAYXJbSjhUgIUFhU$9?8$v{!|xL9V0uQO6p!08*p$R8FZn6`H=Qe;4{PsG$CSg= z-Px1V-wC~IIlF_z#--JWkdUaE9c`KzDy5L$&@~VX6o6DZ&UOph8euG3g-Wd zR_-E;tG|%p+8VOa2GBR&%c3^FN15YJ6^2ZQ{*HW`z0A97Fmho*A(hjW_4Ia6Qx19R z!zG8?iU;_W9d87x8i^OaVpAWxq6DW&c7i*T|L>+pO}P_T)pSi@0)@FBM^nZvJ+I{CaBOX2OEPVr+bAfzq- zE&|2MmC9KxSp+K}GjjTk&Mn8z>dUAGWOgD~E4JLa$#x8qY@fe;%kc!ZrKtd1&&5L-OQ&Dr*O>0eoY||EQ(>tr6}Ly zy`9KR#R)!_$kcn0vV{pLUvF64(rlM=JKA=@A-sL*OEYgq4@-2NjQ+&aCp#%o9o|2#c8hHYn-L2t4P2TmhZ9uwR~)0i5B zkjAy&C*==*BvpKX=HGx$tu$Zuo9X4}c1VnHua4S{vbwr$XKtTPoZZ*A8QN46SO9(D z-^fkWK>nvu76#Wo$@n$5h~5jtEDWlxG`LO7^HtUtRjleLDm!Z)!?#l?!UjS05xJL2 zZPQh{eVsJ2cW*qoeMTY3q4|Nrzz_ysX&oT`V!LzhqguId*Q(T)6H_g#AV(`Qj;8E% zC)d1lCbyelOr;yM;!BOF-pnIjcYS^i<|0Lr-*mkaBr~$&`v+DV&VHEJ7DqiEFn<#O z7XmLE8VpF2we=!2XPi`S@%_px3dtw77m@m&TvH2(H4eY)=**&p*moiA$=DXUOYOZrUZtk^t#NNXhTc@Ofh%HE?;2gB%HE`jW9)|?W<2)@hHQj@vr|E8SJzC83 zm_yhzH>0-1Sg9}HueiI_b;<1RCFEoo&l|`jMfZz8R6XnYP{m*RXDu82uzh^=!$Ya%v&`fsDW_(`;6_S6c?7d} zC#ljzT02g9v;Z!89e*q4og|M7xFImh=4{D~1Utfa9R%xih*KjJ4=nMrja_6KI&f(t zn5$y-BRLwGtGo(n5N$l6e>}ZSud;uiE6}wc#K-_plA{kBZ%TG&m{RG2gfP~4nYm!V zCh58C`jhM&TqTqV*6#Lm;Vi$5{tBM6lXB41y{!0(UQPV&)mjTZ$uO;;p@+&tuhtrW zS{eVc@|9EaXiQ7z9QhEBXhGVDvvQEfg`tU4%_ZM$k$213i^sJ}f9Q)KZ6-}!&+nCR z=0mAN6J59`M6t51#xcAdDlJLXA=cPw-!7(|Wob!@%m+S~mQ)w0MT8fiN>ks7>D2pL z&}3d_vA;Oj>dyV8Ukml*s$t20cv>WSJ@1G_+7P`fd;SzPX9L|=YYX8Xv4U*bc4l;$ z_%kV&alg&CMouma)Z`GSO(;gZpk#Tf>^o~d#?of`NiL#Be%>2eJz$^pUxQPwt-n5> ziCMGrs10uI9FkB4Gi>iIYB9Q)9xS4rRs6L2sMjEEliXXZdjlsd(i+?sGBR!R zV@@G`r^>f12^oILHP11&O#rf=2;rKOU^J9#PJ+Py_a!Jp5gjI1^@$UO`7Xb Tb0!;H2L{ZHaE8_T?y>&>=1;Y~ literal 0 HcmV?d00001 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; + } + } + } }