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).
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user