QueryService: Return application/json and better errors (#84234)

This commit is contained in:
Ryan McKinley
2024-03-19 16:52:15 +03:00
committed by GitHub
parent d1f791cf1f
commit e27c08cfa9
10 changed files with 293 additions and 92 deletions

View File

@@ -0,0 +1,79 @@
package query
import (
"errors"
"fmt"
"github.com/grafana/grafana/pkg/util/errutil"
)
var QueryError = errutil.BadRequest("query.error").MustTemplate(
"failed to execute query [{{ .Public.refId }}]: {{ .Error }}",
errutil.WithPublic(
"failed to execute query [{{ .Public.refId }}]: {{ .Public.error }}",
))
func MakeQueryError(refID, err error) error {
var pErr error
var utilErr errutil.Error
// See if this is grafana error, if so, grab public message
if errors.As(err, &utilErr) {
pErr = utilErr.Public()
} else {
pErr = err
}
data := errutil.TemplateData{
Public: map[string]any{
"refId": refID,
"error": pErr.Error(),
},
Error: err,
}
return QueryError.Build(data)
}
func MakePublicQueryError(refID, err string) error {
data := errutil.TemplateData{
Public: map[string]any{
"refId": refID,
"error": err,
},
}
return QueryError.Build(data)
}
var depErrStr = "did not execute expression [{{ .Public.refId }}] due to a failure to of the dependent expression or query [{{.Public.depRefId}}]"
var dependencyError = errutil.BadRequest("sse.dependencyError").MustTemplate(
depErrStr,
errutil.WithPublic(depErrStr))
func makeDependencyError(refID, depRefID string) error {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
"depRefId": depRefID,
},
Error: fmt.Errorf("did not execute expression %v due to a failure to of the dependent expression or query %v", refID, depRefID),
}
return dependencyError.Build(data)
}
var cyclicErrStr = "cyclic reference in expression [{{ .Public.refId }}]"
var cyclicErr = errutil.BadRequest("sse.cyclic").MustTemplate(
cyclicErrStr,
errutil.WithPublic(cyclicErrStr))
func makeCyclicError(refID string) error {
data := errutil.TemplateData{
Public: map[string]interface{}{
"refId": refID,
},
Error: fmt.Errorf("cyclic reference in %s", refID),
}
return cyclicErr.Build(data)
}

View File

@@ -0,0 +1,21 @@
package query_test
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/util/errutil"
)
func TestQueryErrorType(t *testing.T) {
qet := expr.QueryError
utilError := errutil.Error{}
qe := expr.MakeQueryError("A", "", fmt.Errorf("not work"))
require.True(t, errors.Is(qe, qet))
require.True(t, errors.As(qe, &utilError))
}

View File

@@ -81,11 +81,11 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe
for _, q := range input.Queries {
_, found := queryRefIDs[q.RefID]
if found {
return rsp, fmt.Errorf("multiple queries found for refId: %s", q.RefID)
return rsp, MakePublicQueryError(q.RefID, "multiple queries with same refId")
}
_, found = expressions[q.RefID]
if found {
return rsp, fmt.Errorf("multiple queries found for refId: %s", q.RefID)
return rsp, MakePublicQueryError(q.RefID, "multiple queries with same refId")
}
ds, err := p.getValidDataSourceRef(ctx, q.Datasource, q.DatasourceID)
@@ -161,7 +161,7 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe
if !ok {
target, ok = expressions[refId]
if !ok {
return rsp, fmt.Errorf("expression [%s] is missing variable [%s]", exp.RefID, refId)
return rsp, makeDependencyError(exp.RefID, refId)
}
}
// Do not hide queries used in variables
@@ -169,7 +169,7 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe
q.Hide = false
}
if target.ID() == exp.ID() {
return rsp, fmt.Errorf("expression [%s] can not depend on itself", exp.RefID)
return rsp, makeCyclicError(refId)
}
dg.SetEdge(dg.NewEdge(target, exp))
}
@@ -178,7 +178,7 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe
// Add the sorted expressions
sortedNodes, err := topo.SortStabilized(dg, nil)
if err != nil {
return rsp, fmt.Errorf("cyclic references in query")
return rsp, makeCyclicError("")
}
for _, v := range sortedNodes {
if v.ID() > 0 {

View File

@@ -75,39 +75,41 @@ func TestQuerySplitting(t *testing.T) {
continue
}
fpath := path.Join("testdata", file.Name())
// nolint:gosec
body, err := os.ReadFile(fpath)
require.NoError(t, err)
harness := &parserTestObject{}
err = json.Unmarshal(body, harness)
require.NoError(t, err)
t.Run(file.Name(), func(t *testing.T) {
fpath := path.Join("testdata", file.Name())
// nolint:gosec
body, err := os.ReadFile(fpath)
require.NoError(t, err)
harness := &parserTestObject{}
err = json.Unmarshal(body, harness)
require.NoError(t, err)
changed := false
parsed, err := parser.parseRequest(ctx, &harness.Request)
if err != nil {
if !assert.Equal(t, harness.Error, err.Error(), "File %s", file) {
changed = true
}
} else {
x, _ := json.Marshal(parsed)
y, _ := json.Marshal(harness.Expect)
if !assert.JSONEq(t, string(y), string(x), "File %s", file) {
changed = true
}
}
if changed {
harness.Error = ""
harness.Expect = parsed
changed := false
parsed, err := parser.parseRequest(ctx, &harness.Request)
if err != nil {
harness.Error = err.Error()
if !assert.Equal(t, harness.Error, err.Error(), "File %s", file) {
changed = true
}
} else {
x, _ := json.Marshal(parsed)
y, _ := json.Marshal(harness.Expect)
if !assert.JSONEq(t, string(y), string(x), "File %s", file) {
changed = true
}
}
jj, err := json.MarshalIndent(harness, "", " ")
require.NoError(t, err)
err = os.WriteFile(fpath, jj, 0600)
require.NoError(t, err)
}
if changed {
harness.Error = ""
harness.Expect = parsed
if err != nil {
harness.Error = err.Error()
}
jj, err := json.MarshalIndent(harness, "", " ")
require.NoError(t, err)
err = os.WriteFile(fpath, jj, 0600)
require.NoError(t, err)
}
})
}
})
}

View File

@@ -46,10 +46,7 @@ func (b *QueryAPIBuilder) doQuery(w http.ResponseWriter, r *http.Request) {
errutil.WithPublicMessage(err.Error())), w)
return
}
errhttp.Write(ctx, errutil.BadRequest(
"query.parse",
errutil.WithPublicMessage("Error parsing query")).
Errorf("error parsing: %w", err), w)
errhttp.Write(ctx, err, w)
return
}
@@ -63,6 +60,7 @@ func (b *QueryAPIBuilder) doQuery(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(query.GetResponseCode(rsp))
_ = json.NewEncoder(w).Encode(rsp)
}
@@ -112,7 +110,7 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d
return &backend.QueryDataResponse{}, nil
}
// headers?
// Add user headers... here or in client.QueryData
client, err := b.client.GetDataSourceClient(ctx, v0alpha1.DataSourceRef{
Type: req.PluginId,
UID: req.UID,
@@ -121,9 +119,8 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d
return nil, err
}
// headers?
_, rsp, err := client.QueryData(ctx, *req.Request)
if err == nil {
if err == nil && rsp != nil {
for _, q := range req.Request.Queries {
if q.ResultAssertions != nil {
result, ok := rsp.Responses[q.RefID]

View File

@@ -25,5 +25,5 @@
]
},
"expect": {},
"error": "cyclic references in query"
"error": "[sse.cyclic] cyclic reference in expression []"
}

View File

@@ -16,5 +16,5 @@
]
},
"expect": {},
"error": "expression [A] can not depend on itself"
"error": "[sse.cyclic] cyclic reference in expression [A]"
}